/* 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 com.activiti6.controller.image;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.activiti.engine.*;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.bpmn.behavior.BoundaryEventActivityBehavior;
import org.activiti.engine.impl.bpmn.behavior.CallActivityBehavior;
import org.activiti.engine.impl.bpmn.parser.BpmnParse;
import org.activiti.engine.impl.bpmn.parser.ErrorEventDefinition;
import org.activiti.engine.impl.bpmn.parser.EventSubscriptionDeclaration;
import org.activiti.engine.impl.jobexecutor.TimerDeclarationImpl;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.delegate.ActivityBehavior;
import org.activiti.engine.impl.pvm.process.*;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.runtime.Execution;
import org.activiti.engine.runtime.ProcessInstance;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.*;

public class MyBaseProcessDefinitionDiagramLayoutResource {

  @Autowired
  private RuntimeService runtimeService;

  @Autowired
  private RepositoryService repositoryService;

  @Autowired
  private HistoryService historyService;

  public ObjectNode getDiagramNode(String processInstanceId, String processDefinitionId) {

    List<String> highLightedFlows = Collections.<String> emptyList();
    List<String> highLightedActivities = Collections.<String> emptyList();

    Map<String, ObjectNode> subProcessInstanceMap = new HashMap<String, ObjectNode>();

    ProcessInstance processInstance = null;
    if (processInstanceId != null) {
      processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
      if (processInstance == null) {
        throw new ActivitiObjectNotFoundException("Process instance could not be found");
      }
      processDefinitionId = processInstance.getProcessDefinitionId();

      List<ProcessInstance> subProcessInstances = runtimeService
          .createProcessInstanceQuery()
          .superProcessInstanceId(processInstanceId).list();

      for (ProcessInstance subProcessInstance : subProcessInstances) {
        String subDefId = subProcessInstance.getProcessDefinitionId();

        String superExecutionId = ((ExecutionEntity) subProcessInstance)
            .getSuperExecutionId();
        ProcessDefinitionEntity subDef = (ProcessDefinitionEntity) repositoryService.getProcessDefinition(subDefId);

        ObjectNode processInstanceJSON = new ObjectMapper().createObjectNode();
        processInstanceJSON.put("processInstanceId", subProcessInstance.getId());
        processInstanceJSON.put("superExecutionId", superExecutionId);
        processInstanceJSON.put("processDefinitionId", subDef.getId());
        processInstanceJSON.put("processDefinitionKey", subDef.getKey());
        processInstanceJSON.put("processDefinitionName", subDef.getName());

        subProcessInstanceMap.put(superExecutionId, processInstanceJSON);
      }
    }

    if (processDefinitionId == null) {
      throw new ActivitiObjectNotFoundException("No process definition id provided");
    }

    ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity) repositoryService.getProcessDefinition(processDefinitionId);

    if (processDefinition == null) {
      throw new ActivitiException("Process definition " + processDefinitionId + " could not be found");
    }

    ObjectNode responseJSON = new ObjectMapper().createObjectNode();

    // Process definition
    JsonNode pdrJSON = getProcessDefinitionResponse(processDefinition);

    if (pdrJSON != null) {
      responseJSON.put("processDefinition", pdrJSON);
    }

    // Highlighted activities
    if (processInstance != null) {
      ArrayNode activityArray = new ObjectMapper().createArrayNode();
      ArrayNode flowsArray = new ObjectMapper().createArrayNode();

      highLightedActivities = runtimeService.getActiveActivityIds(processInstanceId);
      highLightedFlows = getHighLightedFlows(processInstanceId, processDefinition);

      for (String activityName : highLightedActivities) {
        activityArray.add(activityName);
      }

      for (String flow : highLightedFlows) {
        flowsArray.add(flow);
      }

      responseJSON.put("highLightedActivities", activityArray);
      responseJSON.put("highLightedFlows", flowsArray);
    }

    // Pool shape, if process is participant in collaboration
    if (processDefinition.getParticipantProcess() != null) {
      ParticipantProcess pProc = processDefinition.getParticipantProcess();

      ObjectNode participantProcessJSON = new ObjectMapper().createObjectNode();
      participantProcessJSON.put("id", pProc.getId());
      if (StringUtils.isNotEmpty(pProc.getName())) {
        participantProcessJSON.put("name", pProc.getName());
      } else {
        participantProcessJSON.put("name", "");
      }
      participantProcessJSON.put("x", pProc.getX());
      participantProcessJSON.put("y", pProc.getY());
      participantProcessJSON.put("width", pProc.getWidth());
      participantProcessJSON.put("height", pProc.getHeight());

      responseJSON.put("participantProcess", participantProcessJSON);
    }

    // Draw lanes

    if (processDefinition.getLaneSets() != null && !processDefinition.getLaneSets().isEmpty()) {
      ArrayNode laneSetArray = new ObjectMapper().createArrayNode();
      for (LaneSet laneSet : processDefinition.getLaneSets()) {
        ArrayNode laneArray = new ObjectMapper().createArrayNode();
        if (laneSet.getLanes() != null && !laneSet.getLanes().isEmpty()) {
          for (Lane lane : laneSet.getLanes()) {
            ObjectNode laneJSON = new ObjectMapper().createObjectNode();
            laneJSON.put("id", lane.getId());
            if (StringUtils.isNotEmpty(lane.getName())) {
              laneJSON.put("name", lane.getName());
            } else {
              laneJSON.put("name", "");
            }
            laneJSON.put("x", lane.getX());
            laneJSON.put("y", lane.getY());
            laneJSON.put("width", lane.getWidth());
            laneJSON.put("height", lane.getHeight());

            List<String> flowNodeIds = lane.getFlowNodeIds();
            ArrayNode flowNodeIdsArray = new ObjectMapper().createArrayNode();
            for (String flowNodeId : flowNodeIds) {
              flowNodeIdsArray.add(flowNodeId);
            }
            laneJSON.put("flowNodeIds", flowNodeIdsArray);

            laneArray.add(laneJSON);
          }
        }
        ObjectNode laneSetJSON = new ObjectMapper().createObjectNode();
        laneSetJSON.put("id", laneSet.getId());
        if (StringUtils.isNotEmpty(laneSet.getName())) {
          laneSetJSON.put("name", laneSet.getName());
        } else {
          laneSetJSON.put("name", "");
        }
        laneSetJSON.put("lanes", laneArray);

        laneSetArray.add(laneSetJSON);
      }

      if (laneSetArray.size() > 0)
        responseJSON.put("laneSets", laneSetArray);
    }

    ArrayNode sequenceFlowArray = new ObjectMapper().createArrayNode();
    ArrayNode activityArray = new ObjectMapper().createArrayNode();

    // Activities and their sequence-flows

    for (ActivityImpl activity : processDefinition.getActivities()) {
      getActivity(processInstanceId, activity, activityArray, sequenceFlowArray,
          processInstance, highLightedFlows, subProcessInstanceMap);
    }

    responseJSON.put("activities", activityArray);
    responseJSON.put("sequenceFlows", sequenceFlowArray);

    return responseJSON;
  }

  private List<String> getHighLightedFlows(String processInstanceId, ProcessDefinitionEntity processDefinition) {

    List<String> highLightedFlows = new ArrayList<String>();
    List<HistoricActivityInstance> historicActivityInstances = historyService
        .createHistoricActivityInstanceQuery()
        .processInstanceId(processInstanceId)
        .orderByHistoricActivityInstanceStartTime().asc().list();

    List<String> historicActivityInstanceList = new ArrayList<String>();
    for (HistoricActivityInstance hai : historicActivityInstances) {
      historicActivityInstanceList.add(hai.getActivityId());
    }

    // add current activities to list
    List<String> highLightedActivities = runtimeService.getActiveActivityIds(processInstanceId);
    historicActivityInstanceList.addAll(highLightedActivities);

    // activities and their sequence-flows
    for (ActivityImpl activity : processDefinition.getActivities()) {
      int index = historicActivityInstanceList.indexOf(activity.getId());

      if (index >= 0 && index + 1 < historicActivityInstanceList.size()) {
        List<PvmTransition> pvmTransitionList = activity
            .getOutgoingTransitions();
        for (PvmTransition pvmTransition : pvmTransitionList) {
          String destinationFlowId = pvmTransition.getDestination().getId();
          if (destinationFlowId.equals(historicActivityInstanceList.get(index + 1))) {
            highLightedFlows.add(pvmTransition.getId());
          }
        }
      }
    }
    return highLightedFlows;
  }

  private void getActivity(String processInstanceId, ActivityImpl activity, ArrayNode activityArray,
      ArrayNode sequenceFlowArray, ProcessInstance processInstance, List<String> highLightedFlows,
      Map<String, ObjectNode> subProcessInstanceMap) {

    ObjectNode activityJSON = new ObjectMapper().createObjectNode();

    // Gather info on the multi instance marker
    String multiInstance = (String) activity.getProperty("multiInstance");
    if (multiInstance != null) {
      if (!"sequential".equals(multiInstance)) {
        multiInstance = "parallel";
      }
    }

    ActivityBehavior activityBehavior = activity.getActivityBehavior();
    // Gather info on the collapsed marker
    Boolean collapsed = (activityBehavior instanceof CallActivityBehavior);
    Boolean expanded = (Boolean) activity.getProperty(BpmnParse.PROPERTYNAME_ISEXPANDED);
    if (expanded != null) {
      collapsed = !expanded;
    }

    Boolean isInterrupting = null;
    if (activityBehavior instanceof BoundaryEventActivityBehavior) {
      isInterrupting = ((BoundaryEventActivityBehavior) activityBehavior).isInterrupting();
    }

    // Outgoing transitions of activity
    for (PvmTransition sequenceFlow : activity.getOutgoingTransitions()) {
      String flowName = (String) sequenceFlow.getProperty("name");
      boolean isHighLighted = (highLightedFlows.contains(sequenceFlow.getId()));
      boolean isConditional = sequenceFlow.getProperty(BpmnParse.PROPERTYNAME_CONDITION) != null &&
          !((String) activity.getProperty("type")).toLowerCase().contains("gateway");
      boolean isDefault = sequenceFlow.getId().equals(activity.getProperty("default"))
          && ((String) activity.getProperty("type")).toLowerCase().contains("gateway");

      List<Integer> waypoints = ((TransitionImpl) sequenceFlow).getWaypoints();
      ArrayNode xPointArray = new ObjectMapper().createArrayNode();
      ArrayNode yPointArray = new ObjectMapper().createArrayNode();
      for (int i = 0; i < waypoints.size(); i += 2) { // waypoints.size()
                                                      // minimally 4: x1, y1,
                                                      // x2, y2
        xPointArray.add(waypoints.get(i));
        yPointArray.add(waypoints.get(i + 1));
      }

      ObjectNode flowJSON = new ObjectMapper().createObjectNode();
      flowJSON.put("id", sequenceFlow.getId());
      flowJSON.put("name", flowName);
      flowJSON.put("flow", "(" + sequenceFlow.getSource().getId() + ")--"
          + sequenceFlow.getId() + "-->("
          + sequenceFlow.getDestination().getId() + ")");

      if (isConditional)
        flowJSON.put("isConditional", isConditional);
      if (isDefault)
        flowJSON.put("isDefault", isDefault);
      if (isHighLighted)
        flowJSON.put("isHighLighted", isHighLighted);

      flowJSON.put("xPointArray", xPointArray);
      flowJSON.put("yPointArray", yPointArray);

      sequenceFlowArray.add(flowJSON);
    }

    // Nested activities (boundary events)
    ArrayNode nestedActivityArray = new ObjectMapper().createArrayNode();
    for (ActivityImpl nestedActivity : activity.getActivities()) {
      nestedActivityArray.add(nestedActivity.getId());
    }

    Map<String, Object> properties = activity.getProperties();
    ObjectNode propertiesJSON = new ObjectMapper().createObjectNode();
    for (String key : properties.keySet()) {
      Object prop = properties.get(key);
      if (prop instanceof String)
        propertiesJSON.put(key, (String) properties.get(key));
      else if (prop instanceof Integer)
        propertiesJSON.put(key, (Integer) properties.get(key));
      else if (prop instanceof Boolean)
        propertiesJSON.put(key, (Boolean) properties.get(key));
      else if ("initial".equals(key)) {
        ActivityImpl act = (ActivityImpl) properties.get(key);
        propertiesJSON.put(key, act.getId());
      } else if ("timerDeclarations".equals(key)) {
        ArrayList<TimerDeclarationImpl> timerDeclarations = (ArrayList<TimerDeclarationImpl>) properties.get(key);
        ArrayNode timerDeclarationArray = new ObjectMapper().createArrayNode();

        if (timerDeclarations != null)
          for (TimerDeclarationImpl timerDeclaration : timerDeclarations) {
            ObjectNode timerDeclarationJSON = new ObjectMapper().createObjectNode();

            timerDeclarationJSON.put("isExclusive", timerDeclaration.isExclusive());
            if (timerDeclaration.getRepeat() != null)
              timerDeclarationJSON.put("repeat", timerDeclaration.getRepeat());

            timerDeclarationJSON.put("retries", String.valueOf(timerDeclaration.getRetries()));
            timerDeclarationJSON.put("type", timerDeclaration.getJobHandlerType());
            timerDeclarationJSON.put("configuration", timerDeclaration.getJobHandlerConfiguration());
            //timerDeclarationJSON.put("expression", timerDeclaration.getDescription());

            timerDeclarationArray.add(timerDeclarationJSON);
          }
        if (timerDeclarationArray.size() > 0)
          propertiesJSON.put(key, timerDeclarationArray);
        // TODO: implement getting description
      } else if ("eventDefinitions".equals(key)) {
        ArrayList<EventSubscriptionDeclaration> eventDefinitions = (ArrayList<EventSubscriptionDeclaration>) properties.get(key);
        ArrayNode eventDefinitionsArray = new ObjectMapper().createArrayNode();

        if (eventDefinitions != null) {
          for (EventSubscriptionDeclaration eventDefinition : eventDefinitions) {
            ObjectNode eventDefinitionJSON = new ObjectMapper().createObjectNode();

            if (eventDefinition.getActivityId() != null)
              eventDefinitionJSON.put("activityId",eventDefinition.getActivityId());

            eventDefinitionJSON.put("eventName", eventDefinition.getEventName());
            eventDefinitionJSON.put("eventType", eventDefinition.getEventType());
            eventDefinitionJSON.put("isAsync", eventDefinition.isAsync());
            eventDefinitionJSON.put("isStartEvent", eventDefinition.isStartEvent());
            eventDefinitionsArray.add(eventDefinitionJSON);
          }
        }

        if (eventDefinitionsArray.size() > 0)
          propertiesJSON.put(key, eventDefinitionsArray);

      // TODO: implement it
      } else if ("errorEventDefinitions".equals(key)) {
        ArrayList<ErrorEventDefinition> errorEventDefinitions = (ArrayList<ErrorEventDefinition>) properties.get(key);
        ArrayNode errorEventDefinitionsArray = new ObjectMapper().createArrayNode();

        if (errorEventDefinitions != null) {
          for (ErrorEventDefinition errorEventDefinition : errorEventDefinitions) {
            ObjectNode errorEventDefinitionJSON = new ObjectMapper().createObjectNode();

            if (errorEventDefinition.getErrorCode() != null)
              errorEventDefinitionJSON.put("errorCode", errorEventDefinition.getErrorCode());
            else
              errorEventDefinitionJSON.putNull("errorCode");

            errorEventDefinitionJSON.put("handlerActivityId",
            errorEventDefinition.getHandlerActivityId());

            errorEventDefinitionsArray.add(errorEventDefinitionJSON);
          }
        }

        if (errorEventDefinitionsArray.size() > 0)
          propertiesJSON.put(key, errorEventDefinitionsArray);
      }

    }

    if ("callActivity".equals(properties.get("type"))) {
      CallActivityBehavior callActivityBehavior = null;

      if (activityBehavior instanceof CallActivityBehavior) {
        callActivityBehavior = (CallActivityBehavior) activityBehavior;
      }

      if (callActivityBehavior != null) {
        propertiesJSON.put("processDefinitonKey", callActivityBehavior.getProcessDefinitonKey());

        // get processDefinitonId from execution or get last processDefinitonId
        // by key
        ArrayNode processInstanceArray = new ObjectMapper().createArrayNode();
        if (processInstance != null) {
          List<Execution> executionList = runtimeService.createExecutionQuery()
              .processInstanceId(processInstanceId)
              .activityId(activity.getId()).list();
          if (!executionList.isEmpty()) {
            for (Execution execution : executionList) {
              ObjectNode processInstanceJSON = subProcessInstanceMap.get(execution.getId());
              processInstanceArray.add(processInstanceJSON);
            }
          }
        }

        // If active activities nas no instance of this callActivity then add
        // last definition
        if (processInstanceArray.size() == 0 && StringUtils.isNotEmpty(callActivityBehavior.getProcessDefinitonKey())) {
          // Get last definition by key
          ProcessDefinition lastProcessDefinition = repositoryService
              .createProcessDefinitionQuery()
              .processDefinitionKey(callActivityBehavior.getProcessDefinitonKey())
              .latestVersion().singleResult();

          // TODO: unuseful fields there are processDefinitionName, processDefinitionKey
          if (lastProcessDefinition != null) {
            ObjectNode processInstanceJSON = new ObjectMapper().createObjectNode();
            processInstanceJSON.put("processDefinitionId", lastProcessDefinition.getId());
            processInstanceJSON.put("processDefinitionKey", lastProcessDefinition.getKey());
            processInstanceJSON.put("processDefinitionName", lastProcessDefinition.getName());
            processInstanceArray.add(processInstanceJSON);
          }
        }

        if (processInstanceArray.size() > 0) {
          propertiesJSON.put("processDefinitons", processInstanceArray);
        }
      }
    }

    activityJSON.put("activityId", activity.getId());
    activityJSON.put("properties", propertiesJSON);
    if (multiInstance != null)
      activityJSON.put("multiInstance", multiInstance);
    if (collapsed)
      activityJSON.put("collapsed", collapsed);
    if (nestedActivityArray.size() > 0)
      activityJSON.put("nestedActivities", nestedActivityArray);
    if (isInterrupting != null)
      activityJSON.put("isInterrupting", isInterrupting);

    activityJSON.put("x", activity.getX());
    activityJSON.put("y", activity.getY());
    activityJSON.put("width", activity.getWidth());
    activityJSON.put("height", activity.getHeight());

    activityArray.add(activityJSON);

    // Nested activities (boundary events)
    for (ActivityImpl nestedActivity : activity.getActivities()) {
      getActivity(processInstanceId, nestedActivity, activityArray, sequenceFlowArray,
          processInstance, highLightedFlows, subProcessInstanceMap);
    }
  }

  private JsonNode getProcessDefinitionResponse(ProcessDefinitionEntity processDefinition) {
    ObjectMapper mapper = new ObjectMapper();
    ObjectNode pdrJSON = mapper.createObjectNode();
    pdrJSON.put("id", processDefinition.getId());
    pdrJSON.put("name", processDefinition.getName());
    pdrJSON.put("key", processDefinition.getKey());
    pdrJSON.put("version", processDefinition.getVersion());
    pdrJSON.put("deploymentId", processDefinition.getDeploymentId());
    pdrJSON.put("isGraphicNotationDefined", isGraphicNotationDefined(processDefinition));
    return pdrJSON;
  }

  private boolean isGraphicNotationDefined(ProcessDefinitionEntity processDefinition) {
    return ((ProcessDefinitionEntity) repositoryService
        .getProcessDefinition(processDefinition.getId()))
        .isGraphicalNotationDefined();
  }
}
