package com.webank.wecube.platform.core.service.workflow;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.ManagementService;
import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.history.HistoricActivityInstance;
import org.camunda.bpm.engine.repository.DeploymentWithDefinitions;
import org.camunda.bpm.engine.repository.ProcessDefinition;
import org.camunda.bpm.engine.repository.ProcessDefinitionQuery;
import org.camunda.bpm.engine.runtime.EventSubscription;
import org.camunda.bpm.engine.runtime.EventSubscriptionQuery;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.runtime.ProcessInstanceQuery;
import org.camunda.bpm.engine.task.Task;
import org.camunda.bpm.model.bpmn.Bpmn;
import org.camunda.bpm.model.bpmn.BpmnModelInstance;
import org.camunda.bpm.model.bpmn.instance.FlowNode;
import org.camunda.bpm.model.bpmn.instance.StartEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.webank.wecube.platform.core.commons.WecubeCoreException;
import com.webank.wecube.platform.core.dto.workflow.ProcDefInfoDto;
import com.webank.wecube.platform.core.dto.workflow.TaskNodeDefInfoDto;
import com.webank.wecube.platform.workflow.WorkflowConstants;
import com.webank.wecube.platform.workflow.entity.ProcessInstanceStatusEntity;
import com.webank.wecube.platform.workflow.entity.ServiceNodeStatusEntity;
import com.webank.wecube.platform.workflow.model.ProcDefOutline;
import com.webank.wecube.platform.workflow.model.ProcFlowNode;
import com.webank.wecube.platform.workflow.model.ProcFlowNodeInst;
import com.webank.wecube.platform.workflow.model.ProcInstOutline;
import com.webank.wecube.platform.workflow.model.ServiceInvocationEvent;
import com.webank.wecube.platform.workflow.model.TraceStatus;
import com.webank.wecube.platform.workflow.parse.BpmnCustomizationException;
import com.webank.wecube.platform.workflow.parse.BpmnParseAttachment;
import com.webank.wecube.platform.workflow.parse.BpmnProcessModelCustomizer;
import com.webank.wecube.platform.workflow.parse.SubProcessAdditionalInfo;
import com.webank.wecube.platform.workflow.repository.ProcessInstanceStatusMapper;
import com.webank.wecube.platform.workflow.repository.ServiceNodeStatusMapper;

/**
 * 
 * @author gavin
 *
 */
@Service
public class WorkflowEngineService {

    private static final Logger log = LoggerFactory.getLogger(WorkflowEngineService.class);

    public static final int SERVICE_TASK_EXECUTE_SUCC = 0;
    public static final int SERVICE_TASK_EXECUTE_ERR = 1;

    public static final String PROCEED_ACT_RETRY = "retry";
    public static final String PROCEED_ACT_SKIP = "skip";

    public static final String PREFIX_EXCEPT_SUB_USER_TASK = "exceptSubUT-";

    private static final String BPMN_SUFFIX = ".bpmn20.xml";

    private String encoding = "UTF-8";

    @Autowired
    protected RepositoryService repositoryService;

    @Autowired
    protected RuntimeService runtimeService;

    @Autowired
    protected HistoryService historyService;

    @Autowired
    protected ManagementService managementService;

    @Autowired
    protected ProcessInstanceStatusMapper processInstanceStatusRepository;

    @Autowired
    protected ServiceNodeStatusMapper serviceNodeStatusRepository;

    @Autowired
    protected TaskService taskService;

    private List<String> statelessNodeTypes = Arrays.asList("startEvent", "endEvent", "exclusiveGateway",
            "parallelGateway");

    public void deleteProcessInstance(String processInstanceId){
        String deleteReason = "Aborted by admin user";
        runtimeService.deleteProcessInstance(processInstanceId, deleteReason);
    }
    
    public String getTaskNodeStatus(String procInstanceId, String nodeId) {
        ServiceNodeStatusEntity nodeStatusEntity = serviceNodeStatusRepository
                .selectOneByProcInstanceIdAndNodeId(procInstanceId, nodeId);

        if (nodeStatusEntity == null) {
            return null;
        }

        if (nodeStatusEntity.getStatus() == null) {
            return null;
        }

        return nodeStatusEntity.getStatus().name();
    }

    public void proceedProcessInstance(String procInstanceId, String nodeId, String userAction) {
        String instanceId = procInstanceId;
        String taskDefKey = PREFIX_EXCEPT_SUB_USER_TASK + nodeId;

        String act = StringUtils.isBlank(userAction) ? PROCEED_ACT_RETRY : userAction;

        if (!(PROCEED_ACT_RETRY.equals(act) || PROCEED_ACT_SKIP.equals(act))) {
            log.warn("invalid action for procInstanceId={} nodeId={} userAction={}", procInstanceId, nodeId,
                    userAction);
            throw new IllegalArgumentException(String.format("action {%s} is invalid.", userAction));
        }

        Task task = taskService.createTaskQuery().processInstanceId(instanceId).active().taskDefinitionKey(taskDefKey)
                .singleResult();

        if (task == null) {
            log.warn("cannot find task with instanceId {} and taskId {}", instanceId, taskDefKey);
            throw new WecubeCoreException("3192", "process instance restarting failed");
        } else {

            String actVarName = String.format("%s_%s", WorkflowConstants.VAR_KEY_USER_ACT, nodeId);

            log.info("to complete task {} put {} {}", taskDefKey, WorkflowConstants.VAR_KEY_USER_ACT, act);
            Map<String, Object> variables = new HashMap<String, Object>();
            variables.put(actVarName, act);

            taskService.complete(task.getId(), variables);
        }
    }

    public void handleServiceInvocationResult(ServiceInvocationEvent event) {
        String procInstId = event.getInstanceId();
        String procInstKey = event.getBusinessKey();
        String executionId = event.getExecutionId();

        if (log.isInfoEnabled()) {
            log.info("handling service invocation result,procInstId={},procInstKey={},executionId={},resultCode={}",
                    procInstId, procInstKey, executionId, event.getResult());
        }

        String resultCode = event.getResult();

        ProcessInstance instance = null;

        for (int i = 0; i < 20; i++) {
            instance = runtimeService.createProcessInstanceQuery().processInstanceId(procInstId).singleResult();
            if (instance != null) {
                break;
            }

            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                log.warn("Interrupted Exception: " + e.getMessage());
                Thread.currentThread().interrupt();
            }

            log.debug("retried {} times", (i + 1));
        }

        if (instance == null) {
            log.info("Cannot find process instance with such id, procInstKey={}", procInstKey);
            throw new RuntimeException("None process instance found.");
        }

        EventSubscription signalEventSubscription = null;

        for (int i = 0; i < 20; i++) {
            EventSubscriptionQuery eventSubscriptionQuery = runtimeService.createEventSubscriptionQuery()
                    .eventType("signal").processInstanceId(instance.getProcessInstanceId());

            if (StringUtils.isNotBlank(executionId)) {
                eventSubscriptionQuery = eventSubscriptionQuery.activityId(executionId);
            }
            List<EventSubscription> signalEventSubscriptions = eventSubscriptionQuery.list();

            if (!signalEventSubscriptions.isEmpty()) {
                signalEventSubscription = signalEventSubscriptions.get(0);
            }

            if (signalEventSubscription != null) {
                break;
            }

            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                log.warn("Interrupt errors", e);
            }
        }

        if (signalEventSubscription == null) {
            log.info("Such subscription have not found for event:{} {} {}", procInstId, procInstKey, executionId);
            return;
        }

        String eventName = signalEventSubscription.getEventName();
        Map<String, Object> boundVariables = new HashMap<String, Object>();

        String varName = String.format("retCode_%s", executionId);
        boundVariables.put(varName, resultCode);
        log.debug("put {}, {}", varName, resultCode);

        try {
            tryEmitSignalEvent(eventName, signalEventSubscription, resultCode, procInstId, procInstKey, boundVariables);
        } catch (Exception e) {
            log.warn("Errors while emitting signal event", e);
        }

    }

    private void tryEmitSignalEvent(String eventName, EventSubscription signalEventSubscription, String resultCode,
            String procInstId, String procInstKey, Map<String, Object> boundVariables) {
        for (int times = 0; times <= 10; times++) {

            try {
                log.info(
                        "###### {} try delivering {} to execution {}, serviceCode {}, instanceId {}, businessKey {} activityId {}",
                        times, eventName, signalEventSubscription.getId(), resultCode, procInstId, procInstKey,
                        signalEventSubscription.getActivityId());
                runtimeService.createSignalEvent(eventName).executionId(signalEventSubscription.getExecutionId())
                        .setVariables(boundVariables).send();
                break;
            } catch (Exception e) {
                log.info("Errors while sending signal " + resultCode + " -" + e.getMessage());
                log.info(
                        "######Failed {} delivering {} to execution {}, serviceCode {}, instanceId {}, businessKey {} activityId {}",
                        times, eventName, signalEventSubscription.getId(), resultCode, procInstId, procInstKey,
                        signalEventSubscription.getActivityId());

                try {
                    Thread.sleep(300);
                } catch (InterruptedException e1) {
                    log.warn("Interrupted exceptions", e1);
                }
            }
        }

    }

    public ProcInstOutline getProcInstOutline(String procInstId) {
        if (procInstId == null) {
            throw new WecubeCoreException("3193", "Process instance is null.");
        }

        ProcessInstanceStatusEntity procInstStatusEntity = processInstanceStatusRepository
                .selectOneByProcInstanceId(procInstId);

        if (procInstStatusEntity == null) {
            log.warn("cannot find such process instance record with procInstId={}", procInstId);
            throw new WecubeCoreException("3194", "Such process instance record does not exist.");
        }

        String processInstanceId = null;
        String processDefinitionId = null;
        if (TraceStatus.Completed.equals(procInstStatusEntity.getStatus())) {
            processInstanceId = procInstStatusEntity.getProcInstId();
            processDefinitionId = procInstStatusEntity.getProcDefId();
        } else {
            ProcessInstance existProcInst = getProcessInstanceByProcInstId(procInstId);

            if (existProcInst == null) {
                log.warn("such process instance does not exist,procInstId={}", procInstId);
                throw new WecubeCoreException("3195", "Such process instance does not exist.");
            }

            processInstanceId = existProcInst.getId();
            processDefinitionId = existProcInst.getProcessDefinitionId();
        }

        ProcessDefinition procDef = getProcessDefinitionByProcId(processDefinitionId);

        if (procDef == null) {
            log.warn("such process definition does not exist,procDefId={}", processDefinitionId);
            throw new WecubeCoreException("3196", "Such process definition does not exist.");
        }

        BpmnModelInstance bpmnModel = repositoryService.getBpmnModelInstance(procDef.getId());

        Collection<org.camunda.bpm.model.bpmn.instance.Process> processes = bpmnModel
                .getModelElementsByType(org.camunda.bpm.model.bpmn.instance.Process.class);

        org.camunda.bpm.model.bpmn.instance.Process process = processes.iterator().next();

        Collection<StartEvent> startEvents = process.getChildElementsByType(StartEvent.class);

        StartEvent startEvent = startEvents.iterator().next();

        ProcInstOutline result = new ProcInstOutline();
        result.setId(processInstanceId);
        result.setProcInstKey(procInstStatusEntity.getProcInstKey());
        result.setProcDefKernelId(procDef.getId());
        result.setProcDefKey(procDef.getKey());
        result.setProcDefName(procDef.getName());
        result.setStatus(procInstStatusEntity.getStatus().name());

        populateFlowNodeInsts(result, startEvent);
        refreshFlowNodeStatus(result);

        return result;
    }

    protected void refreshFlowNodeStatus(ProcInstOutline outline) {
        for (ProcFlowNodeInst n : outline.getNodeInsts()) {
            String nodeType = n.getNodeType();

            if (!statelessNodeTypes.contains(nodeType)) {
                continue;
            }

            String nodeStatus = tryCalculateStatelessNodeStatus(outline, n);

            if (nodeStatus == null) {
                Collection<HistoricActivityInstance> activities = historyService.createHistoricActivityInstanceQuery()
                        .processDefinitionId(outline.getProcDefKernelId()).processInstanceId(outline.getId())
                        .activityId(n.getId()).finished().list();
                HistoricActivityInstance activity = null;

                if (activities.size() > 0) {
                    activity = activities.iterator().next();
                }

                if (activity != null) {
                    nodeStatus = TraceStatus.Completed.name();
                }
            }

            if (nodeStatus != null) {
                n.setStatus(nodeStatus);
            }

        }
    }

    protected String tryCalculateStatelessNodeStatus(ProcInstOutline outline, ProcFlowNodeInst nodeInst) {
        String nodeStatus = null;

        if ("startEvent".equalsIgnoreCase(nodeInst.getNodeType())) {
            nodeStatus = tryCalStartEventNodeStatus(outline, nodeInst);
            return nodeStatus;
        }

        if ("endEvent".equalsIgnoreCase(nodeInst.getNodeType())) {
            nodeStatus = tryCalEndEventNodeStatus(outline, nodeInst);
            return nodeStatus;
        }

        if ("exclusiveGateway".equalsIgnoreCase(nodeInst.getNodeType())) {
            nodeStatus = tryCalExclusiveGatewayNodeStatus(outline, nodeInst);
            return nodeStatus;
        }

        if ("parallelGateway".equalsIgnoreCase(nodeInst.getNodeType())) {
            nodeStatus = tryCalParallelGatewayNodeStatus(outline, nodeInst);
            return nodeStatus;
        }

        return null;

    }

    private String tryCalExclusiveGatewayNodeStatus(ProcInstOutline outline, ProcFlowNodeInst nodeInst) {
        String nodeStatus = null;
        boolean isOnePreviousFlowNodesCompleted = false;

        for (ProcFlowNode child : nodeInst.getPreviousFlowNodes()) {
            ProcFlowNodeInst fi = (ProcFlowNodeInst) child;
            if (TraceStatus.Completed.name().equals(fi.getStatus())) {
                isOnePreviousFlowNodesCompleted = true;
                break;
            }
        }

        if (isOnePreviousFlowNodesCompleted) {
            nodeStatus = TraceStatus.Completed.name();
        } else {
            nodeStatus = TraceStatus.NotStarted.name();
        }

        return nodeStatus;
    }

    private String tryCalParallelGatewayNodeStatus(ProcInstOutline outline, ProcFlowNodeInst nodeInst) {
        String nodeStatus = null;
        boolean isAllPreviousFlowNodesCompleted = true;

        for (ProcFlowNode child : nodeInst.getPreviousFlowNodes()) {
            ProcFlowNodeInst fi = (ProcFlowNodeInst) child;
            if (!TraceStatus.Completed.name().equals(fi.getStatus())) {
                isAllPreviousFlowNodesCompleted = false;
                break;
            }
        }

        if (isAllPreviousFlowNodesCompleted) {
            nodeStatus = TraceStatus.Completed.name();
        } else {
            nodeStatus = TraceStatus.NotStarted.name();
        }

        if (!TraceStatus.Completed.name().equals(nodeStatus)) {
            boolean isOneSucceedingFlowNodeStarted = false;

            for (ProcFlowNode child : nodeInst.getSucceedingFlowNodes()) {
                ProcFlowNodeInst fi = (ProcFlowNodeInst) child;
                if (fi.getStatus() != null && !TraceStatus.NotStarted.name().equals(fi.getStatus())) {
                    isOneSucceedingFlowNodeStarted = true;
                    break;
                }
            }

            if (isOneSucceedingFlowNodeStarted) {
                nodeStatus = TraceStatus.Completed.name();
            }
        }

        return nodeStatus;
    }

    private String tryCalStartEventNodeStatus(ProcInstOutline outline, ProcFlowNodeInst nodeInst) {
        return TraceStatus.Completed.name();
    }

    private String tryCalEndEventNodeStatus(ProcInstOutline outline, ProcFlowNodeInst nodeInst) {
        String nodeStatus = null;
        boolean isOnePreviousFlowNodesCompleted = false;

        for (ProcFlowNode child : nodeInst.getPreviousFlowNodes()) {
            ProcFlowNodeInst fi = (ProcFlowNodeInst) child;
            if (TraceStatus.Completed.name().equals(fi.getStatus())) {
                isOnePreviousFlowNodesCompleted = true;
                break;
            }
        }

        if (isOnePreviousFlowNodesCompleted) {
            nodeStatus = TraceStatus.Completed.name();
        } else {
            nodeStatus = TraceStatus.NotStarted.name();
        }

        return nodeStatus;
    }

    protected void populateFlowNodeInsts(ProcInstOutline outline, FlowNode flowNode) {
        ProcFlowNodeInst pfn = outline.findProcFlowNodeInstByNodeId(flowNode.getId());
        if (pfn == null) {
            pfn = new ProcFlowNodeInst();
            pfn.setId(flowNode.getId());
            pfn.setNodeType(flowNode.getElementType().getTypeName());
            pfn.setNodeName(flowNode.getName() == null ? "" : flowNode.getName());
            outline.addNodeInsts(pfn);
        }

        ServiceNodeStatusEntity nodeStatus = serviceNodeStatusRepository
                .selectOneByProcInstanceIdAndNodeId(outline.getId(), pfn.getId());

        if (nodeStatus != null) {
            pfn.setStartTime(nodeStatus.getStartTime());
            pfn.setEndTime(nodeStatus.getEndTime());
            pfn.setStatus(nodeStatus.getStatus().name());
        }

        for (FlowNode fn : flowNode.getSucceedingNodes().list()) {
            ProcFlowNodeInst childPfn = outline.findProcFlowNodeInstByNodeId(fn.getId());
            boolean needPopulateChild = false;
            if (childPfn == null) {
                childPfn = new ProcFlowNodeInst();
                childPfn.setId(fn.getId());
                childPfn.setNodeType(fn.getElementType().getTypeName());
                childPfn.setNodeName(fn.getName() == null ? "" : fn.getName());
                outline.addNodeInsts(childPfn);
                needPopulateChild = true;
            }

            pfn.addSucceedingFlowNodes(childPfn);

            if (needPopulateChild) {
                populateFlowNodeInsts(outline, fn);
            }
        }

    }

    protected ProcessInstance getProcessInstanceByProcInstId(String processInstanceId) {
        ProcessInstance procInst = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId)
                .singleResult();
        return procInst;
    }

    public ProcessDefinition deployProcessDefinition(ProcDefInfoDto procDefDto) {
        try {
            return doDeployProcessDefinition(procDefDto);
        } catch (Exception e) {
            log.warn("errors while deploy process definition", e);
            throw new BpmnCustomizationException(e.getMessage());
        }
    }

    public ProcessInstance retrieveProcessInstance(String processInstanceId) {
        if (StringUtils.isBlank(processInstanceId)) {
            throw new IllegalArgumentException("Process instance ID is blank.");
        }

        ProcessInstanceQuery query = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId);

        ProcessInstance procInst = query.singleResult();

        if (procInst == null) {
            throw new WecubeCoreException("3197",
                    String.format("Such process instance with id [%s] does not exist.", processInstanceId),
                    processInstanceId);
        }

        return procInst;
    }

    public ProcessDefinition retrieveProcessDefinition(String processDefinitionId) {
        if (StringUtils.isBlank(processDefinitionId)) {
            throw new WecubeCoreException("3198", "Process definition ID is blank.");
        }
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(processDefinitionId);
        ProcessDefinition procDef = query.singleResult();
        if (procDef == null) {
            log.warn("such process definition did not exist,processDefinitionId={}", processDefinitionId);
        }
        return procDef;
    }

    public ProcessInstance startProcessInstance(String processDefinitionId, String processInstanceKey) {
        if (StringUtils.isBlank(processDefinitionId)) {
            throw new IllegalArgumentException("Process definition ID is blank.");
        }

        if (log.isDebugEnabled()) {
            log.debug("try to start process instance with id {} and key {}", processDefinitionId, processInstanceKey);
        }

        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(processDefinitionId);
        ProcessDefinition procDef = query.singleResult();
        if (procDef == null) {
            log.warn("such process definition did not exist,processDefinitionId={}", processDefinitionId);
            throw new WecubeCoreException("3199",
                    String.format("Such proccess definition [%s] does not exist.", processDefinitionId),
                    processDefinitionId);
        }

        ProcessInstance instance = runtimeService.startProcessInstanceById(processDefinitionId, processInstanceKey);

        if (instance == null) {
            log.warn("Failed to create process instance with id {} and key {}", processDefinitionId,
                    processInstanceKey);
            throw new WecubeCoreException("3200",
                    String.format("Failed to create process instance with id %s and key %s.", processDefinitionId,
                            processInstanceKey),
                    processDefinitionId, processInstanceKey);
        }

        return instance;
    }

    protected ProcessDefinition doDeployProcessDefinition(ProcDefInfoDto procDefDto) {
        String fileName = procDefDto.getProcDefName() + BPMN_SUFFIX;
        BpmnParseAttachment bpmnParseAttachment = buildBpmnParseAttachment(procDefDto.getTaskNodeInfos());

        BpmnProcessModelCustomizer customizer = new BpmnProcessModelCustomizer(fileName, procDefDto.getProcDefData(),
                encoding);
        customizer.setBpmnParseAttachment(bpmnParseAttachment);
        BpmnModelInstance procModelInstance = customizer.build();

        DeploymentWithDefinitions deployment = repositoryService.createDeployment()
                .addModelInstance(fileName, procModelInstance).deployWithResult();
        List<ProcessDefinition> processDefs = deployment.getDeployedProcessDefinitions();

        if (processDefs == null || processDefs.isEmpty()) {
            log.warn("abnormally to parse process definition,request={}", procDefDto);
            throw new WecubeCoreException("3201", "Process deploying failed");
        }

        ProcessDefinition processDef = processDefs.get(0);

        return processDef;
    }

    public ProcDefOutline readProcDefOutlineFromXmlData(String xmlData) {
        BpmnModelInstance bpmnModel = readModelFromXmlData(xmlData);

        ProcDefOutline pdo = new ProcDefOutline();

        Collection<org.camunda.bpm.model.bpmn.instance.Process> processes = bpmnModel
                .getModelElementsByType(org.camunda.bpm.model.bpmn.instance.Process.class);

        org.camunda.bpm.model.bpmn.instance.Process process = processes.iterator().next();

        // pdo.setProcDefKey(process.getKey());
        pdo.setProcDefName(process.getName());

        Collection<StartEvent> startEvents = process.getChildElementsByType(StartEvent.class);

        StartEvent startEvent = startEvents.iterator().next();

        populateFlowNodes(pdo, startEvent);

        return pdo;
    }

    protected BpmnModelInstance readModelFromXmlData(String xmlData) {
        if (StringUtils.isBlank(xmlData)) {
            throw new WecubeCoreException("3202", "XML data to parse cannot be blank.");
        }

        InputStream is = null;
        BpmnModelInstance procModelInstance = null;
        try {
            is = new ByteArrayInputStream(xmlData.getBytes(encoding));
            procModelInstance = Bpmn.readModelFromStream(is);
        } catch (UnsupportedEncodingException e1) {
            log.warn("errors while reading model", e1);
            procModelInstance = null;
            throw new WecubeCoreException("3203", "Failed to read xml process content");
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    log.warn("errors while closing", e);
                }
            }
        }

        if (procModelInstance == null) {
            log.warn("failed to read model instance.");
            throw new WecubeCoreException("3204", "Failed to read model instance.");
        }

        return procModelInstance;
    }

    public ProcDefOutline getProcDefOutline(ProcessDefinition procDef) {
        ProcDefOutline pdo = new ProcDefOutline();
        pdo.setId(procDef.getId());
        pdo.setProcDefKey(procDef.getKey());
        pdo.setProcDefName(procDef.getName());

        BpmnModelInstance bpmnModel = repositoryService.getBpmnModelInstance(procDef.getId());

        Collection<org.camunda.bpm.model.bpmn.instance.Process> processes = bpmnModel
                .getModelElementsByType(org.camunda.bpm.model.bpmn.instance.Process.class);

        org.camunda.bpm.model.bpmn.instance.Process process = processes.iterator().next();

        Collection<StartEvent> startEvents = process.getChildElementsByType(StartEvent.class);

        StartEvent startEvent = startEvents.iterator().next();

        populateFlowNodes(pdo, startEvent);

        return pdo;
    }

    protected void populateFlowNodes(ProcDefOutline outline, FlowNode flowNode) {
        ProcFlowNode pfn = outline.findFlowNode(flowNode.getId());
        if (pfn == null) {
            pfn = buildProcFlowNode(flowNode);
            outline.addFlowNodes(pfn);
        }

        Collection<FlowNode> succeedingFlowNodes = flowNode.getSucceedingNodes().list();

        for (FlowNode fn : succeedingFlowNodes) {
            ProcFlowNode childPfn = outline.findFlowNode(fn.getId());
            boolean needPopulateChild = false;
            if (childPfn == null) {
                childPfn = buildProcFlowNode(fn);
                outline.addFlowNodes(childPfn);
                needPopulateChild = true;
            }

            pfn.addSucceedingFlowNodes(childPfn);

            if (needPopulateChild) {
                populateFlowNodes(outline, fn);
            }
        }

    }

    protected ProcessDefinition getProcessDefinitionByProcId(String processDefinitionId) {
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .processDefinitionId(processDefinitionId).singleResult();
        return procDef;
    }

    protected ProcFlowNode buildProcFlowNode(FlowNode fn) {
        ProcFlowNode pfn = new ProcFlowNode();
        pfn.setId(fn.getId());
        pfn.setNodeName(fn.getName() == null ? "" : fn.getName());
        pfn.setNodeType(fn.getElementType().getTypeName());

        return pfn;
    }

    private BpmnParseAttachment buildBpmnParseAttachment(List<TaskNodeDefInfoDto> taskNodeInfoDtos) {
        BpmnParseAttachment bpmnParseAttachment = new BpmnParseAttachment();

        for (TaskNodeDefInfoDto dto : taskNodeInfoDtos) {
            SubProcessAdditionalInfo info = new SubProcessAdditionalInfo();
            info.setSubProcessNodeId(dto.getNodeId());
            info.setSubProcessNodeName(dto.getNodeName());
            info.setTimeoutExpression(convertIsoTimeFormat(dto.getTimeoutExpression()));

            bpmnParseAttachment.addSubProcessAddtionalInfo(info);
        }

        return bpmnParseAttachment;
    }

    private String convertIsoTimeFormat(String timeoutExpression) {
        if (StringUtils.isBlank(timeoutExpression)) {
            return timeoutExpression;
        }

        return "PT" + timeoutExpression.trim() + "M";
    }
}
