/*
 * Author: Chuntung Ho <chuntungho@gmail.com>
 *
 */

package com.yayamagic.tool.tool;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.yayamagic.tool.Application;
import com.yayamagic.tool.config.Config;
import com.yayamagic.tool.i18n.I18nSupport;
import com.yayamagic.tool.log.Logger;
import com.yayamagic.tool.misc.Promptable;
import com.yayamagic.tool.processor.DefaultProcessor;
import com.yayamagic.tool.simulator.Series;
import com.yayamagic.tool.simulator.Simulator;
import com.yayamagic.tool.state.StateController;
//import org.jdom.Document;
//import org.jdom.Element;
//import org.jdom.input.SAXBuilder;

public class ActivityTool extends DefaultProcessor implements Tool,
	ActivityConstants {
    static Logger logger = Logger.getLogger(ActivityTool.class);

    public static final String DEFAULT_CONFIG = "/config/activity.xml";
    private Map<String, String> properties = new HashMap<String, String>();
    private Map<String, Simulator> simulators = new HashMap<String, Simulator>();
    private Map<String, Series> series = new HashMap<String, Series>();

    private StateController stateController = StateController.getInstance();
    private List<ActivityListener> activityListeners = new ArrayList<ActivityListener>();

    private String configFile = DEFAULT_CONFIG;
    private String activityId = null;

    public ActivityTool() {
	// If not any super constructor is specified, implicit constructor
	// super() will be added by default.
	// So if super type implicit constructor is undifined, there would be a
	// compile error. Must explicitly invoke another super constructor.
	super((Promptable) Application.getMainFrame());
    }

    /**
     * Constructor.
     * 
     * @param config
     *            The xml config file name.
     * @param activityId
     *            The activity id defined in xml name.
     */
    public ActivityTool(String config, String activityId) {
	super((Promptable) Application.getMainFrame());
	if (config != null) {
	    this.configFile = "/config/" + config;
	}

	if (activityId != null) {
	    this.activityId = activityId;
	}
    }

    /**
     * Get property defined in definition tag, can be used for enrich method.
     * 
     * @param key
     *            property name
     * @return property value
     */
    public String getProperty(String key) {
	return properties.get(key);
    }

    /**
     * Get simulator defined in definition tag, can be used for onSimulate
     * method.
     * 
     * @param key
     *            The id of simualtor
     * @return The simulator
     */
    public Simulator getSimulator(String key) {
	return simulators.get(key);
    }

    /**
     * Get series defined in definition tag, can be used for onSimulate method.
     * 
     * @param key
     *            The id of series
     * @return The series
     */
    public Series getSeries(String key) {
	return series.get(key);
    }

    @Override
    public void work() {
	if (stateController.getCurrentState() != StateController.STOP) {
	    return;
	}
	stateController.setSimulatorProcessor(this);
	stateController.switchState();
    }

    @Override
    public void run() {
	stateController.getCurrentState().notifyGUI();

	Document doc = loadDocument();
	Element root = doc.getDocumentElement();
	Element acitivity = null;
	initDefinition((Element) root.getElementsByTagName(DEFINITION).item(0));
	NodeList activities = root.getElementsByTagName(ACTIVITY);
	if (this.activityId != null) {
	    for (int i = 0; i < activities.getLength(); i++) {
		acitivity = (Element) activities.item(i);
		if (activityId.equals(acitivity.getAttribute("id"))) {
		    execActivity(acitivity);
		}
	    }
	} else {
	    acitivity = (Element) activities.item(0);
	    execActivity(acitivity);
	}

	reset();
	prompt(I18nSupport.getString("processor.completed"));
	for (ActivityListener listener : activityListeners) {
	    listener.complete();
	}
    }

    // Execute the activity in another thread
    private void execActivity(Element e) {
	String listenerClass = e.getAttribute("listener");

	if (listenerClass != null) {
	    addActivityListener(listenerClass);
	}

	NodeList children = e.getElementsByTagName("*");
	int len = children.getLength();
	for (int i = 0; i < len; i++) {
	    Element child = (Element) children.item(i);
	    if (checkStop()) {
		return;
	    }

	    simulate(child);
	}
    }

    private boolean checkStop() {
	if (stopping) {
	    stateController.getCurrentState().notifyGUI();
	    stateController.setSimulatorProcessor(null);
	    prompt(I18nSupport.getString("processor.stopped"));
	    return true;
	}
	return stopping;
    }

    /**
     * 
     * @param e
     *            The activity element
     */
    private void simulate(Element e) {
	String name, ref;
	name = e.getTagName();
	if (STEP.equals(name)) {
	    Simulator simulator = null;
	    ref = e.getAttribute("ref");
	    simulator = simulators.get(ref);

	    for (ActivityListener listener : activityListeners) {
		simulator = listener.onSimulate(ref, simulator);
	    }

	    if (simulator != null) {
		simulator.simulate(robot);
		prompt("Executed step [" + ref + "]");
	    }
	}

	if (LOOP.equals(name)) {
	    int times = 0;
	    List<Simulator> list = null;
	    ref = e.getAttribute("ref");
	    if (ref != null && !ref.isEmpty()) {
		list = series.get(ref).getSimulators();
		times = list.size();
		prompt("Executing step [" + ref + "]");
	    } else {
		times = Integer.valueOf(e.getAttribute("times"));
	    }

	    Simulator beforeSimulator = null;
	    Simulator simulator = null;
	    String beforeStep = e.getAttribute("beforeStep");

	    if (beforeStep != null && !beforeStep.isEmpty()) {
		beforeSimulator = simulators.get(beforeStep);
	    }

	    for (int i = 0; i < times; i++) {
		if (checkStop()) {
		    return;
		}

		if (beforeSimulator != null) {
		    beforeSimulator.simulate(robot);
		}

		if (list != null) {
		    simulator = list.get(i);
		    for (ActivityListener listener : activityListeners) {
			simulator = listener.onSimulate(ref, simulator);
		    }

		    if (simulator != null) {
			simulator.simulate(robot);
		    }
		}

		prompt("Executed step [" + ref + "] " + (i + 1));

		// recursion for series children
		NodeList children = e.getElementsByTagName("*");
		int len = children.getLength();
		for (int j = 0; j < len; j++) {
		    Element child = (Element) children.item(j);
		    if (checkStop()) {
			return;
		    }
		    simulate(child);
		}
	    }
	}
    }

    // Initialize simualtors and series
    private void initDefinition(Element e) {
	NodeList children;
	Element child;
	int len;
	children = e.getElementsByTagName(PROPERTY);
	len = children.getLength();
	for (int i = 0; i < len; i++) {
	    child = (Element) children.item(i);
	    properties.put(child.getAttribute("name"), child.getTextContent()
		    .trim());
	}

	if (properties.get(LISTENER) != null) {
	    addActivityListener(properties.get(LISTENER));
	}

	children = e.getElementsByTagName(SIMULATOR);
	len = children.getLength();
	for (int i = 0; i < len; i++) {
	    child = (Element) children.item(i);
	    simulators.put(child.getAttribute("id"),
		    (Simulator) getInstance(child));
	}

	children = e.getElementsByTagName(SERIES);
	len = children.getLength();
	for (int i = 0; i < len; i++) {
	    child = (Element) children.item(i);
	    series.put(child.getAttribute("id"), (Series) getInstance(child));
	}
    }

    private void addActivityListener(String clazz) {
	ActivityListener listener = null;
	try {
	    listener = (ActivityListener) Class.forName(clazz).newInstance();
	    listener.setRobot(robot);
	    listener.setContext(this);
	    activityListeners.add(listener);
	} catch (Exception e1) {
	    logger.error("add Listerner exception", e1);
	}
    }

    private Object getInstance(Element e) {
	String id = e.getAttribute("id");
	String className = e.getAttribute("class");
	if (className == null || className.isEmpty()) {
	    className = properties.get("default" + e.getTagName() + "Class");
	}
	Object obj = null;
	try {
	    obj = Class.forName(className).newInstance();
	    NodeList props = e.getElementsByTagName("Property");
	    int len = props.getLength();
	    for (int i = 0; i < len; i++) {
		Element prop = (Element) props.item(i);
		String field = prop.getAttribute("name");
		String value = prop.getTextContent().trim();
		setProperty(obj, field, value, String.class);
	    }
	} catch (Exception e1) {
	    logger.error("instance exception", e1);
	}

	// notify listeners
	for (ActivityListener listener : activityListeners) {
	    listener.enrich(id, obj);
	}

	return obj;
    }

    // Simple reflection to set property, only support String value
    private void setProperty(Object obj, String property, Object value,
	    Class<?>... clazz) {
	String methodName = "set" + property.substring(0, 1).toUpperCase()
		+ property.substring(1);
	Method m;
	try {
	    m = obj.getClass().getMethod(methodName, clazz);
	    m.invoke(obj, value);
	} catch (Exception e) {
	    logger.error(e);
	}
    }

    // Load document from file or class path
    private Document loadDocument() {
	// Document doc = null;
	// try {
	// URL url = Config.getURL(configFile);
	// SAXBuilder builder = new SAXBuilder();
	// doc = builder.build(url);
	// } catch (Exception e) {
	// logger.error(e);
	// } finally {
	// }

	Document doc = null;
	DocumentBuilder builder;
	try {
	    builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
	    doc = builder.parse(Config.getURL(configFile).toString());
	} catch (Exception e) {
	    logger.error("Failed to parse", e);
	}
	return doc;
    }
}