package com.huigou.bpm.management.translate.bpmn;

import com.huigou.bpm.management.domain.model.ProcessDefinition;
import com.huigou.util.StringUtil;
import com.mxgraph.io.mxCodec;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.util.mxXmlUtils;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.*;
import org.activiti.engine.delegate.ExecutionListener;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author yonghuan
 * @since 1.2.0
 */
final class MxGraphXmlParser {

    private final static String ATTRIBUTE_CODE = "code";
    private final static String ATTRIBUTE_LABEL = "label";

    private final static List<String> IGNORED_MX_CELL_IDS = Arrays.asList("0", "1");
    private final static String SHAPE_CONNECTOR = "connector";
    private final static String SHAPE_TASK = "task";
    private final static String SHAPE_GATEWAY_XOR = "gateway_xor";
    private final static String SHAPE_GATEWAY_AND = "gateway_and";
    private final static String SHAPE_GATEWAY_OR = "gateway_or";
    private final static String SHAPE_BUSINESS_RULE_TASK = "business_rule_task";
    private final static String SHAPE_GENERAL_START = "general_start";
    private final static String SHAPE_GENERAL_END = "general_end";

    private final BpmnModel bpmnModel = new BpmnModel();
    private final Process process;
    private final mxGraphModel graphModel;

    private final List<String> procInstExecutionListenerBeanName;
    private final String bizTaskExecutionListenerBeanName;
    private final String serviceExecutorBeanName;

    /**
     * 所有连线
     */
    private final List<mxCell> edges;
    /**
     * 所有形状
     */
    private final List<mxCell> shapes;

    /**
     * @param processDefinition 流程定义
     */
    MxGraphXmlParser(ProcessDefinition processDefinition, String procInstExecutionListenerBeanName,
                     String bizTaskExecutionListenerBeanName, String serviceExecutorBeanName) {
        this.procInstExecutionListenerBeanName = Arrays.asList(StringUtils.split(procInstExecutionListenerBeanName, ","));
        this.bizTaskExecutionListenerBeanName = bizTaskExecutionListenerBeanName;
        this.serviceExecutorBeanName = serviceExecutorBeanName;

        Document mxGraphXmlDocument = mxXmlUtils.parseXml(processDefinition.getXml());
        mxCodec codec = new mxCodec(mxGraphXmlDocument);
        graphModel = (mxGraphModel) codec.decode(mxGraphXmlDocument.getDocumentElement());
        edges = graphModel.getCells().values()
                .stream()
                .map(mxCell.class::cast)
                .filter(mxCell::isEdge)
                .filter(edge -> edge.getSource() != null && edge.getTarget() != null)
                .collect(Collectors.toList());
        shapes = graphModel.getCells().values()
                .stream()
                .map(mxCell.class::cast)
                .filter(cell -> !cell.isEdge())
                .filter(cell -> !IGNORED_MX_CELL_IDS.contains(cell.getId()))
                .collect(Collectors.toList());

        bpmnModel.setTargetNamespace("http://www.activiti.org/" + processDefinition.getCode());
        process = new Process();
        process.setExecutable(true);
        process.setId(processDefinition.getCode());
        process.setName(processDefinition.getName());
        if (this.procInstExecutionListenerBeanName.size() > 0) {
            List<ActivitiListener> listeners = Arrays.asList(ExecutionListener.EVENTNAME_START, ExecutionListener.EVENTNAME_END)
                    .stream()
                    .flatMap(eventName -> createActivitiListener(eventName, this.procInstExecutionListenerBeanName))
                    .collect(Collectors.toList());
            process.setExecutionListeners(listeners);
        }
        bpmnModel.addProcess(process);
    }

    private Stream<ActivitiListener> createActivitiListener(String eventName, List<String> listenerBeanName) {
        return listenerBeanName.stream()
                .map(listener -> {
                    ActivitiListener activitiListener = new ActivitiListener();
                    activitiListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
                    activitiListener.setImplementation("${" + listener + "}");
                    activitiListener.setEvent(eventName);
                    return activitiListener;
                });
    }

    byte[] parse() {
        for (mxCell shape : shapes) {
            if (StringUtil.isBlank(shape.getStyle())) {
                continue;
            }
            String shapeName = Arrays.stream(shape.getStyle().split(";"))
                    .map(s -> s.split("="))
                    .filter(s -> s.length == 2)
                    .collect(Collectors.toMap(s -> s[0], s -> s[1]))
                    .get("shape");
            if (shapeName == null) {
                continue;
            }
            String shapeSimpleName = shapeName.split("\\.")[1];
            switch (shapeSimpleName) {
                case SHAPE_TASK:
                    generateServiceTask(shape);
                    break;
                case SHAPE_GATEWAY_XOR:
                    generateExclusiveGateway(shape);
                    break;
                case SHAPE_GATEWAY_AND:
                    generateParallelGateway(shape);
                    break;
                case SHAPE_GATEWAY_OR:
                    generateInclusiveGateway(shape);
                    break;
                case SHAPE_BUSINESS_RULE_TASK:
                    generateBusinessRuleTask(shape);
                    break;
                case SHAPE_GENERAL_START:
                    generateStartEvent(shape);
                    break;
                case SHAPE_GENERAL_END:
                    generateEndEvent(shape);
                    break;
                default:
                    break;
            }
        }
        // 生成连线
        edges.forEach(this::generateSequenceFlow);
        BpmnXMLConverter converter = new BpmnXMLConverter();
        return converter.convertToXML(bpmnModel);
    }

    private String findCodeOrDefault(mxCell cell) {
        String code = cell.getAttribute(ATTRIBUTE_CODE);
        return StringUtils.isNotBlank(code)
                ? code
                : getNodeShapeSimpleName(cell) + cell.getId();
    }

    /**
     * 生成排他网关。
     */
    private void generateExclusiveGateway(mxCell shape) {
        String code = findCodeOrDefault(shape);
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
        exclusiveGateway.setId(code);
        exclusiveGateway.setName(label);
        process.addFlowElement(exclusiveGateway);
    }

    /**
     * 生成并行网关，并行网关不会走连接线上设置的条件，如果设置了，则会被忽略。
     */
    private void generateParallelGateway(mxCell shape) {
        String code = findCodeOrDefault(shape);
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        ParallelGateway parallelGateway = new ParallelGateway();
        parallelGateway.setId(code);
        parallelGateway.setName(label);
        process.addFlowElement(parallelGateway);
    }

    /**
     * 生成包容网关。
     */
    private void generateInclusiveGateway(mxCell shape) {
        String code = findCodeOrDefault(shape);
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        InclusiveGateway inclusiveGateway = new InclusiveGateway();
        inclusiveGateway.setId(code);
        inclusiveGateway.setName(label);
        process.addFlowElement(inclusiveGateway);
    }

    /**
     * 生成业务规则任务。
     */
    private void generateBusinessRuleTask(mxCell shape) {
        BusinessRuleTask businessRuleTask = new BusinessRuleTask();
        businessRuleTask.setId(shape.getAttribute(ATTRIBUTE_CODE));
        businessRuleTask.setName(shape.getAttribute(ATTRIBUTE_LABEL));
        List<String> ruleNames = Arrays.asList(StringUtils.split(shape.getAttribute("ruleNames"), ","));
        businessRuleTask.setRuleNames(ruleNames);
        List<String> inputVariables = Arrays.asList(StringUtils.split(shape.getAttribute("inputVariables"), ","));
        businessRuleTask.setInputVariables(inputVariables);
        String resultVariableName = shape.getAttribute("resultVariableName");
        businessRuleTask.setResultVariableName(resultVariableName);
        process.addFlowElement(businessRuleTask);
    }

    /**
     * 生成服务任务，转换规则为：
     * <li>如果task是流程的第一个task需要将task转换成一个等待节点和一个等待结束节点，</li>
     * <li>否则转换为一个task节点、一个等待开始节点、一个等待结束节点。</li>
     */
    private void generateServiceTask(mxCell shape) {
        String id = shape.getId();
        String serviceTaskCode = shape.getAttribute(ATTRIBUTE_CODE);
        if (StringUtils.isBlank(serviceTaskCode)) {
            serviceTaskCode = String.join("", SHAPE_TASK, id);
        }
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        // 判断当前task节点是否是流程的第一个task节点。
        boolean needGenerateServiceTask = edges.stream()
                .filter(edge -> edge.getTarget().getId().equals(id))
                .map(edge -> (mxCell) edge.getSource())
                .map(cell -> getNodeShapeSimpleName(cell))
                .anyMatch(shapeSimpleName -> !shapeSimpleName.equals(SHAPE_GENERAL_START));

        // task
        ServiceTask serviceTask = null;
        if (needGenerateServiceTask) {
            serviceTask = new ServiceTask();
            serviceTask.setId(serviceTaskCode);
            serviceTask.setName(label);
            serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
            serviceTask.setImplementation("${" + serviceExecutorBeanName + "}");
            process.addFlowElement(serviceTask);
        }

        // 等待开始消息
        String startMsgId = String.format("start%sMsg", StringUtil.toUpperCamelCase(serviceTaskCode));
        String startIntermediateCatchEventId = String.format("waitStart%s", StringUtil.toUpperCamelCase(serviceTaskCode));
        bpmnModel.addMessage(new Message(startMsgId, startMsgId, ""));
        IntermediateCatchEvent startIntermediateCatchEvent = new IntermediateCatchEvent();
        startIntermediateCatchEvent.setId(startIntermediateCatchEventId);
        startIntermediateCatchEvent.setName(startIntermediateCatchEventId);
        MessageEventDefinition startEventDefinition = new MessageEventDefinition();
        startEventDefinition.setMessageRef(startMsgId);
        startIntermediateCatchEvent.addEventDefinition(startEventDefinition);
        process.addFlowElement(startIntermediateCatchEvent);


        // 等待结束消息
        String endMsgId = String.format("end%sMsg", StringUtil.toUpperCamelCase(serviceTaskCode));
        String endIntermediateCatchEventId = String.format("waitEnd%s", StringUtil.toUpperCamelCase(serviceTaskCode));
        bpmnModel.addMessage(new Message(endMsgId, endMsgId, ""));
        IntermediateCatchEvent endIntermediateCatchEvent = new IntermediateCatchEvent();
        endIntermediateCatchEvent.setId(endIntermediateCatchEventId);
        endIntermediateCatchEvent.setName(endIntermediateCatchEventId);
        MessageEventDefinition endEventDefinition = new MessageEventDefinition();
        endEventDefinition.setMessageRef(endMsgId);
        endIntermediateCatchEvent.addEventDefinition(endEventDefinition);
        process.addFlowElement(endIntermediateCatchEvent);

        // 连接线
        ActivitiListener bizTaskExecutionListener = null;
        if (StringUtils.isNotBlank(bizTaskExecutionListenerBeanName)) {
            bizTaskExecutionListener = new ActivitiListener();
            bizTaskExecutionListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
            bizTaskExecutionListener.setImplementation("${" + bizTaskExecutionListenerBeanName + "}");
            bizTaskExecutionListener.setEvent(ExecutionListener.EVENTNAME_START);
        }
        if (needGenerateServiceTask) {
            if (bizTaskExecutionListener != null) {
                serviceTask.setExecutionListeners(Collections.singletonList(bizTaskExecutionListener));
            }
            generateSequenceFlowInternal(String.format("%sWait0", serviceTaskCode), "", serviceTaskCode, startIntermediateCatchEventId, null);
            generateSequenceFlowInternal(String.format("%sWait1", serviceTaskCode), "", startIntermediateCatchEventId, endIntermediateCatchEventId, null);
        } else {
            if (bizTaskExecutionListener != null) {
                startIntermediateCatchEvent.setExecutionListeners(Collections.singletonList(bizTaskExecutionListener));
            }
            generateSequenceFlowInternal(String.format("%sWait0", serviceTaskCode), "", startIntermediateCatchEventId, endIntermediateCatchEventId, null);
        }
    }

    /**
     * 生成连线。
     */
    private void generateSequenceFlow(mxCell edge) {
        mxCell sourceCell = (mxCell) edge.getSource();
        mxCell targetCell = (mxCell) edge.getTarget();

        String sourceCode = sourceCell.getAttribute(ATTRIBUTE_CODE);
        String targetCode = targetCell.getAttribute(ATTRIBUTE_CODE);
        String code = findCodeOrDefault(edge);
        String label = edge.getAttribute(ATTRIBUTE_LABEL);

        if (SHAPE_TASK.equals(getNodeShapeSimpleName(sourceCell))) {
            // 如果连线的开始节点是一个task节点，那么就需要把连线的开始节点设置为task节点的等待完成节点
            sourceCode = String.format("waitEnd%s", StringUtil.toUpperCamelCase(sourceCode));
        }
        if (SHAPE_TASK.equals(getNodeShapeSimpleName(targetCell)) && SHAPE_GENERAL_START.equals(getNodeShapeSimpleName(sourceCell))) {
            // 如果连线的结束节点是task节点并且开始节点是startEvent节点，那么就需要把连线的结束节点设置为task节点的等待开始节点
            targetCode = String.format("waitStart%s", StringUtil.toUpperCamelCase(targetCode));
        }
        generateSequenceFlowInternal(code, label, sourceCode, targetCode, edge.getAttribute("expression"));
    }

    private void generateSequenceFlowInternal(String code, String name, String sourceCode, String
            targetCode, String expression) {
        SequenceFlow sequenceFlow = new SequenceFlow(sourceCode, targetCode);
        sequenceFlow.setId(code);
        sequenceFlow.setName(name);
        if (StringUtils.isNotBlank(expression)) {
            sequenceFlow.setConditionExpression(expression);
        }
        process.addFlowElement(sequenceFlow);
    }

    private Map<String, String> getStyle(mxCell cell) {
        if (StringUtils.isBlank(cell.getStyle())) {
            return Collections.emptyMap();
        }
        return Arrays.stream(cell.getStyle().split(";"))
                .map(s -> s.split("="))
                .collect(Collectors.toMap(s -> s[0], s -> s[1]));
    }

    private String getStyleValue(mxCell cell, String styleName) {
        return getStyle(cell).get(styleName);
    }

    private String getNodeShapeSimpleName(mxCell cell) {
        String shape = getStyleValue(cell, "shape");
        if (StringUtils.isBlank(shape)) {
            return null;
        }
        return shape.equals(SHAPE_CONNECTOR) ? SHAPE_CONNECTOR : shape.split("\\.")[1];
    }

    /**
     * 生成启动事件。
     */
    private void generateEndEvent(mxCell shape) {
        String code = findCodeOrDefault(shape);
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        EndEvent endEvent = new EndEvent();
        endEvent.setId(code);
        endEvent.setName(label);
        process.addFlowElement(endEvent);
    }

    /**
     * 生成结束事件。
     */
    private void generateStartEvent(mxCell shape) {
        String code = findCodeOrDefault(shape);
        String label = shape.getAttribute(ATTRIBUTE_LABEL);
        StartEvent startEvent = new StartEvent();
        startEvent.setId(code);
        startEvent.setName(label);
        process.addFlowElement(startEvent);
    }
}
