/**
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jbpm.bpmn2.xml;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.drools.compiler.compiler.xml.XmlDumper;
import org.drools.core.xml.ExtensibleXmlParser;
import org.jbpm.bpmn2.core.Error;
import org.jbpm.bpmn2.core.Escalation;
import org.jbpm.bpmn2.core.Message;
import org.jbpm.bpmn2.core.Signal;
import org.jbpm.compiler.xml.ProcessBuildData;
import org.jbpm.process.core.event.EventFilter;
import org.jbpm.process.core.event.EventTransformerImpl;
import org.jbpm.process.core.event.EventTypeFilter;
import org.jbpm.process.core.event.NonAcceptingEventTypeFilter;
import org.jbpm.process.core.impl.DataTransformerRegistry;
import org.jbpm.process.core.timer.Timer;
import org.jbpm.workflow.core.Node;
import org.jbpm.workflow.core.impl.DroolsConsequenceAction;
import org.jbpm.workflow.core.node.ConstraintTrigger;
import org.jbpm.workflow.core.node.EventSubProcessNode;
import org.jbpm.workflow.core.node.EventTrigger;
import org.jbpm.workflow.core.node.StartNode;
import org.jbpm.workflow.core.node.Transformation;
import org.jbpm.workflow.core.node.Trigger;
import org.kie.api.runtime.process.DataTransformer;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

public class StartEventHandler extends AbstractNodeHandler {

	private DataTransformerRegistry transformerRegistry = DataTransformerRegistry.get();

    protected Node createNode(Attributes attrs) {
        return new StartNode();
    }

    @SuppressWarnings("unchecked")
	public Class generateNodeFor() {
        return StartNode.class;
    }

    @SuppressWarnings("unchecked")
    protected void handleNode(final Node node, final Element element, final String uri,
            final String localName, final ExtensibleXmlParser parser) throws SAXException {
        super.handleNode(node, element, uri, localName, parser);
        StartNode startNode = (StartNode) node;
        // TODO: StartEventHandler.handleNode(): the parser doesn't discriminate between the schema default and the actual set value
        // However, while the schema says the "isInterrupting" attr should default to true
        // The spec says that Escalation start events should default to not interrupting..
        startNode.setInterrupting(Boolean.parseBoolean(element.getAttribute("isInterrupting")));

        org.w3c.dom.Node xmlNode = element.getFirstChild();
        while (xmlNode != null) {
            String nodeName = xmlNode.getNodeName();
            if ("dataOutput".equals(nodeName)) {
                readDataOutput(xmlNode, startNode);
            } else if ("dataOutputAssociation".equals(nodeName)) {
                readDataOutputAssociation(xmlNode, startNode);
            } else if ("outputSet".equals(nodeName)) {
                // p. 225, BPMN2 spec (2011-01-03)
                // InputSet and OutputSet elements imply that process execution should wait for them to be filled
                // and are therefore not applicable to catch events
                String message = "Ignoring <" + nodeName + "> element: "
                        + "<" + nodeName + "> elements should not be used on start or other catch events.";
                SAXParseException saxpe = new SAXParseException( message, parser.getLocator() );
                parser.warning(saxpe);
                // no exception thrown for backwards compatibility (we used to ignore these elements)
            } else if ("conditionalEventDefinition".equals(nodeName)) {
                String constraint = null;
                org.w3c.dom.Node subNode = xmlNode.getFirstChild();
                while (subNode != null) {
                    String subnodeName = subNode.getNodeName();
                    if ("condition".equals(subnodeName)) {
                        constraint = xmlNode.getTextContent();
                        break;
                    }
                    subNode = subNode.getNextSibling();
                }
                ConstraintTrigger trigger = new ConstraintTrigger();
                trigger.setConstraint(constraint);
                startNode.addTrigger(trigger);
                break;
            } else if ("signalEventDefinition".equals(nodeName)) {
                String type = ((Element) xmlNode).getAttribute("signalRef");

                type = checkSignalAndConvertToRealSignalNam(parser, type);

                if (type != null && type.trim().length() > 0) {
                    addTriggerWithInMappings(startNode, type);
                }
            } else if ("messageEventDefinition".equals(nodeName)) {
                String messageRef = ((Element) xmlNode).getAttribute("messageRef");
                Map<String, Message> messages = (Map<String, Message>)
                    ((ProcessBuildData) parser.getData()).getMetaData("Messages");
                if (messages == null) {
                    throw new IllegalArgumentException("No messages found");
                }
                Message message = messages.get(messageRef);
                if (message == null) {
                    throw new IllegalArgumentException("Could not find message " + messageRef);
                }
                startNode.setMetaData("MessageType", message.getType());


                addTriggerWithInMappings(startNode, "Message-" + messageRef);
            } else if ("timerEventDefinition".equals(nodeName)) {
            	handleTimerNode(startNode, element, uri, localName, parser);
                // following event definitions are only for event sub process and will be validated to not be included in top process definitions
            } else if ("errorEventDefinition".equals(nodeName)) {
                if( ! startNode.isInterrupting() ) {
                    // BPMN2 spec (p.245-246, (2011-01-03)) implies that
                    //   - a <startEvent> in an Event Sub-Process
                    //    - *without* the 'isInterupting' attribute always interrupts (containing process)
                    String errorMsg = "Error Start Events in an Event Sub-Process always interrupt the containing (sub)process(es).";
                    throw new IllegalArgumentException(errorMsg);
                }
                String errorRef = ((Element) xmlNode).getAttribute("errorRef");
                if (errorRef != null && errorRef.trim().length() > 0) {
                    List<Error> errors = (List<Error>) ((ProcessBuildData) parser.getData()).getMetaData("Errors");
                    if (errors == null) {
                        throw new IllegalArgumentException("No errors found");
                    }
                    Error error = null;
                    for( Error listError : errors ) {
                        if( errorRef.equals(listError.getId()) ) {
                            error = listError;
                        }
                    }
                    if (error == null) {
                        throw new IllegalArgumentException("Could not find error " + errorRef);
                    }
                    startNode.setMetaData("FaultCode", error.getErrorCode());
                    addTriggerWithInMappings(startNode, "Error-" + error.getErrorCode());
                }
            } else if ("escalationEventDefinition".equals(nodeName)) {
                String escalationRef = ((Element) xmlNode).getAttribute("escalationRef");
                if (escalationRef != null && escalationRef.trim().length() > 0) {
                    Map<String, Escalation> escalations = (Map<String, Escalation>)
                        ((ProcessBuildData) parser.getData()).getMetaData(ProcessHandler.ESCALATIONS);
                    if (escalations == null) {
                        throw new IllegalArgumentException("No escalations found");
                    }
                    Escalation escalation = escalations.get(escalationRef);
                    if (escalation == null) {
                        throw new IllegalArgumentException("Could not find escalation " + escalationRef);
                    }

                    addTriggerWithInMappings(startNode, "Escalation-" + escalation.getEscalationCode());
                }
            } else if ("compensateEventDefinition".equals(nodeName)) {
                handleCompensationNode(startNode, element, xmlNode, parser);
            }
            xmlNode = xmlNode.getNextSibling();
        }
    }

    private void addTriggerWithInMappings(StartNode startNode, String triggerEventType) {
        EventTrigger trigger = new EventTrigger();
        EventTypeFilter eventFilter = new EventTypeFilter();
        eventFilter.setType(triggerEventType);
        trigger.addEventFilter(eventFilter);

        String mapping = (String) startNode.getMetaData("TriggerMapping");
        if (mapping != null) {
            trigger.addInMapping(mapping, startNode.getOutMapping(mapping));
        }

        startNode.addTrigger(trigger);
    }

    public Object end(final String uri, final String localName,
            final ExtensibleXmlParser parser) throws SAXException {
        StartNode startNode = (StartNode) super.end(uri, localName, parser);

        return startNode;
    }

    protected void readDataOutputAssociation(org.w3c.dom.Node xmlNode, StartNode startNode) {
        // sourceRef
        org.w3c.dom.Node subNode = xmlNode.getFirstChild();
        if( ! "sourceRef".equals(subNode.getNodeName()) ) {
            throw new IllegalArgumentException("No sourceRef found in dataOutputAssociation in startEvent");
        }
        String source = subNode.getTextContent();
        if( dataOutputs.get(source) == null ) {
           throw new IllegalArgumentException( "No dataOutput could be found for the dataOutputAssociation." );
        }

        // targetRef
        subNode = subNode.getNextSibling();
        if( ! "targetRef".equals(subNode.getNodeName()) ) {
            throw new IllegalArgumentException("No targetRef found in dataOutputAssociation in startEvent");
        }
        String target = subNode.getTextContent();
        startNode.setMetaData("TriggerMapping", target);
        // transformation
  		Transformation transformation = null;
  		subNode = subNode.getNextSibling();
  		if (subNode != null && "transformation".equals(subNode.getNodeName())) {
  			String lang = subNode.getAttributes().getNamedItem("language").getNodeValue();
  			String expression = subNode.getTextContent();
  			DataTransformer transformer = transformerRegistry.find(lang);
  			if (transformer == null) {
  				throw new IllegalArgumentException("No transformer registered for language " + lang);
  			}
  			transformation = new Transformation(lang, expression, dataOutputs.get(source));
  			startNode.setMetaData("Transformation", transformation);

  			startNode.setEventTransformer(new EventTransformerImpl(transformation));
  	        subNode = subNode.getNextSibling();
  		}

        if( subNode != null ) {
            // no support for assignments
            throw new UnsupportedOperationException(subNode.getNodeName() + " elements in dataOutputAssociations are not yet supported.");
        }
        startNode.addOutMapping(target, dataOutputs.get(source));
    }

    // The results of this method are only used to check syntax
    protected void readDataOutput(org.w3c.dom.Node xmlNode, StartNode startNode) {
        String id = ((Element) xmlNode).getAttribute("id");
        String outputName = ((Element) xmlNode).getAttribute("name");
        dataOutputs.put(id, outputName);
    }

    public void writeNode(Node node, StringBuilder xmlDump, int metaDataType) {
		StartNode startNode = (StartNode) node;
		writeNode("startEvent", startNode, xmlDump, metaDataType);
		xmlDump.append(" isInterrupting=\"" );
		if (startNode.isInterrupting()) {
		    xmlDump.append("true");
		} else {
		    xmlDump.append("false");
		}
		xmlDump.append("\">" + EOL);
		writeExtensionElements(startNode, xmlDump);

		List<Trigger> triggers = startNode.getTriggers();
		if (triggers != null) {
		    if (triggers.size() > 1) {
		        throw new IllegalArgumentException("Multiple start triggers not supported");
		    }

		    Trigger trigger = triggers.get(0);
		    if (trigger instanceof ConstraintTrigger) {
		    	ConstraintTrigger constraintTrigger = (ConstraintTrigger) trigger;
		    	if (constraintTrigger.getHeader() == null) {
			        xmlDump.append("      <conditionalEventDefinition>" + EOL);
	                xmlDump.append("        <condition xsi:type=\"tFormalExpression\" language=\"" + XmlBPMNProcessDumper.RULE_LANGUAGE + "\">" + constraintTrigger.getConstraint() + "</condition>" + EOL);
	                xmlDump.append("      </conditionalEventDefinition>" + EOL);
		    	}
		    } else if (trigger instanceof EventTrigger) {
		        EventTrigger eventTrigger = (EventTrigger) trigger;
		        String mapping = null;
		        String nameMapping = "event";
		        if (!trigger.getInMappings().isEmpty()) {
		            mapping = eventTrigger.getInMappings().keySet().iterator().next();
		            nameMapping = eventTrigger.getInMappings().values().iterator().next();
		        }
		        else {
		            mapping = (String) startNode.getMetaData("TriggerMapping");
		        }

		        if( mapping != null ) {
		            xmlDump.append(
	                    "      <dataOutput id=\"_" + startNode.getId() + "_Output\" name=\""+ nameMapping +"\" />" + EOL +
                        "      <dataOutputAssociation>" + EOL +
                        "        <sourceRef>_" + startNode.getId() + "_Output</sourceRef>" + EOL +
                        "        <targetRef>" + mapping + "</targetRef>" + EOL +
                        "      </dataOutputAssociation>" + EOL);
		        }

		        String type = ((EventTypeFilter) eventTrigger.getEventFilters().get(0)).getType();
		        if (type.startsWith("Message-")) {
                    type = type.substring(8);
                    xmlDump.append("      <messageEventDefinition messageRef=\"" + type + "\"/>" + EOL);
                } else if (type.startsWith("Error-")) {
                    type = type.substring(6);
                    String errorId = getErrorIdForErrorCode(type, startNode);
                    xmlDump.append("      <errorEventDefinition errorRef=\"" + XmlBPMNProcessDumper.replaceIllegalCharsAttribute(errorId) + "\"/>" + EOL);
                } else if (type.startsWith("Escalation-")) {
                    type = type.substring(11);
                    xmlDump.append("      <escalationEventDefinition escalationRef=\"" + type + "\"/>" + EOL);
                } else if (type.equals("Compensation")) {
                    xmlDump.append("      <compensateEventDefinition/>" + EOL);
                } else {
                    xmlDump.append("      <signalEventDefinition signalRef=\"" + type + "\" />" + EOL);
                }
            } else {
		        throw new IllegalArgumentException("Unsupported trigger type " + trigger);
		    }

		    if (startNode.getTimer() != null) {
	            Timer timer = startNode.getTimer();
	            xmlDump.append("      <timerEventDefinition>" + EOL);
	            if (timer != null && (timer.getDelay() != null || timer.getDate() != null)) {
	                if (timer.getTimeType() == Timer.TIME_DATE) {
                        xmlDump.append("        <timeDate xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDate()) + "</timeDate>" + EOL);
                    } else if (timer.getTimeType() == Timer.TIME_DURATION) {
	                    xmlDump.append("        <timeDuration xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeDuration>" + EOL);
	                } else if (timer.getTimeType() == Timer.TIME_CYCLE) {

	                    if (timer.getPeriod() != null) {
	                        xmlDump.append("        <timeCycle xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "###" + XmlDumper.replaceIllegalChars(timer.getPeriod()) + "</timeCycle>" + EOL);
	                    } else {
	                        xmlDump.append("        <timeCycle xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeCycle>" + EOL);
	                    }
	                } else if (timer.getTimeType() == Timer.TIME_DATE) {
	                    xmlDump.append("        <timeDate xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeDate>" + EOL);
	                }
	            }
	            xmlDump.append("      </timerEventDefinition>" + EOL);
		    }
		} else if (startNode.getTimer() != null) {
            Timer timer = startNode.getTimer();
            xmlDump.append("      <timerEventDefinition>" + EOL);
            if (timer != null && (timer.getDelay() != null || timer.getDate() != null)) {
                if (timer.getTimeType() == Timer.TIME_DATE) {
                    xmlDump.append("        <timeDate xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDate()) + "</timeDate>" + EOL);
                } else if (timer.getTimeType() == Timer.TIME_DURATION) {
                    xmlDump.append("        <timeDuration xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeDuration>" + EOL);
                } else if (timer.getTimeType() == Timer.TIME_CYCLE) {

                    if (timer.getPeriod() != null) {
                        xmlDump.append("        <timeCycle xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "###" + XmlDumper.replaceIllegalChars(timer.getPeriod()) + "</timeCycle>" + EOL);
                    } else {
                        xmlDump.append("        <timeCycle xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeCycle>" + EOL);
                    }
                } else if (timer.getTimeType() == Timer.TIME_DATE) {
                    xmlDump.append("        <timeDate xsi:type=\"tFormalExpression\">" + XmlDumper.replaceIllegalChars(timer.getDelay()) + "</timeDate>" + EOL);
                }
            }
            xmlDump.append("      </timerEventDefinition>" + EOL);
        }
    	endNode("startEvent", xmlDump);
	}

    protected void handleTimerNode(final Node node, final Element element,
            final String uri, final String localName,
            final ExtensibleXmlParser parser) throws SAXException {
        super.handleNode(node, element, uri, localName, parser);
        StartNode startNode = (StartNode) node;
        org.w3c.dom.Node xmlNode = element.getFirstChild();
        while (xmlNode != null) {
            String nodeName = xmlNode.getNodeName();
            if ("timerEventDefinition".equals(nodeName)) {
                Timer timer = new Timer();
                org.w3c.dom.Node subNode = xmlNode.getFirstChild();
                while (subNode instanceof Element) {
                    String subNodeName = subNode.getNodeName();
                    if ("timeCycle".equals(subNodeName)) {
                        String delay = subNode.getTextContent();
                        int index = delay.indexOf("###");
                        if (index != -1) {
                            String period = delay.substring(index + 3);
                            delay = delay.substring(0, index);
                            timer.setPeriod(period);
                        } else {
                            timer.setPeriod(delay);
                        }
                        timer.setTimeType(Timer.TIME_CYCLE);
                        timer.setDelay(delay);
                        break;
                    } else if ("timeDuration".equals(subNodeName)) {
                        String delay = subNode.getTextContent();
                        timer.setTimeType(Timer.TIME_DURATION);
                        timer.setDelay(delay);
                        break;
                    } else if ("timeDate".equals(subNodeName)) {
                        String date = subNode.getTextContent();
                        timer.setTimeType(Timer.TIME_DATE);
                        timer.setDate(date);
                        break;
                    }
                    subNode = subNode.getNextSibling();
                }
                startNode.setTimer(timer);
                if (parser.getParent() instanceof EventSubProcessNode) {
                  // handle timer on start events like normal (non rule) timers for event sub process

                  EventTrigger trigger = new EventTrigger();
                  EventTypeFilter eventFilter = new EventTypeFilter();
                  eventFilter.setType("Timer-" + ((EventSubProcessNode) parser.getParent()).getId());
                  trigger.addEventFilter(eventFilter);
                  String mapping = (String) startNode.getMetaData("TriggerMapping");
                  if (mapping != null) {
                      trigger.addInMapping(mapping, "event");
                  }
                  startNode.addTrigger(trigger);
                  ((EventSubProcessNode) parser.getParent()).addTimer(timer, new DroolsConsequenceAction("java", ""));
              }
            }
            xmlNode = xmlNode.getNextSibling();
        }
    }

    protected void handleCompensationNode(final StartNode startNode, final Element element, final org.w3c.dom.Node xmlNode,
            final ExtensibleXmlParser parser) throws SAXException {
        if( startNode.isInterrupting() ) {
            logger.warn( "Compensation Event Sub-Processes [" + startNode.getMetaData("UniqueId") + "] may not be specified as interrupting:" +
            		" overriding attribute and setting to not-interrupting.");
        }
        startNode.setInterrupting(false);

        /** From the BPMN2 spec, P.264:
         * "For a Start Event:
         *  This Event "catches" the compensation for an Event Sub-Process. No further information is required.
         *  The Event Sub-Process will provide the id necessary to match the Compensation Event with the Event
         *  that threw the compensation"
         *
         *  In other words, the id of the Sub-Process containing this Event Sub-Process is what should be used
         *  as the activityRef value in any Intermediate (throw) or End compensation event that targets
         *  this particular Event Sub-Process.
         *
         *  This is similar to the logic used for a Compensation Boundary Event: it's signaled using
         *  the id of the activity to which the CBE is attached to.
         */
        String activityRef = ((Element) xmlNode).getAttribute("activityRef");
        if( activityRef != null && activityRef.length() > 0 ) {
            logger.warn("activityRef value [" + activityRef + "] on Start Event '" + startNode.getMetaData("UniqueId")
                    + "' ignored per the BPMN2 specification.");
        }

        // so that this node will get processed in ProcessHandler.postProcessNodes(...)
        EventTrigger startTrigger = new EventTrigger();
        EventFilter eventFilter = new NonAcceptingEventTypeFilter();
        ((NonAcceptingEventTypeFilter) eventFilter).setType("Compensation");
        startTrigger.addEventFilter(eventFilter);
        List<Trigger> startTriggers = new ArrayList<Trigger>();
        startTriggers.add(startTrigger);
        startNode.setTriggers(startTriggers);
        String mapping = (String) startNode.getMetaData("TriggerMapping");
        if (mapping != null) {
            startTrigger.addInMapping(mapping, startNode.getOutMapping(mapping));
        }
    }

}
