"""A module for converting services to BPMN."""
from collections import OrderedDict
import xml.dom.minidom
from lxml import etree
from lxml.etree import QName

_DIGITAL_STR = 'digital'
_TITLE_STR = 'title'
_IMPLEMENTATION_STR = 'implementation'
_CHILD_STR = 'child'
_SEMANTIC_STR = 'semantic'
_DEFINITIONS_STR = 'definitions'
_TIMER_STR = 'Timer'
_MIN_STR = 'min'
_MAX_STR = 'max'
_NUM_ID_SUFFIX = 'num_id'
_PREV_CHILD_SUFFIX = f'previous_{_CHILD_STR}'

_MAX_DURATION_TEMPLATE = {
    'Λεπτά': 'PT{}M',
    'Ώρες': 'PT{}Η',
    'Ημέρες': 'P{}DS',
    'Εβδομάδες': 'P{}WS',
    'Μήνες': 'P{}MS'
}

def bracketize(string):
    return f'{{{string}}}'

def getMaxDurationAsString(timer):
    duration_type = timer[2]
    try:
        template = _MAX_DURATION_TEMPLATE[duration_type]
    except KeyError:
        return ''
    max_duration = timer[1]
    min_duration = timer[0]
    out = max(float(max_duration), float(min_duration))
    return template.format(out)

class BPMNNamespaces:
    def __init__(self, semantic, bpmndi, di, dc, xsi, NSMAP, di_NAMESPACE,
                 xsi_NAMESPACE):
        self.semantic = semantic
        self.bpmndi = bpmndi
        self.di = di
        self.dc = dc
        self.xsi = xsi
        self.NSMAP = NSMAP
        self.di_NAMESPACE = di_NAMESPACE
        self.xsi_NAMESPACE = xsi_NAMESPACE

class BPMN:
    # Keys
    _SEMANTIC_KEY = _SEMANTIC_STR
    _DEFINITIONS_KEY = _DEFINITIONS_STR
    _BPMNDI_KEY = 'bpmndi'
    _DI_KEY = 'di'
    _DC_KEY = 'dc'
    _XSI_KEY = 'xsi'
    _XML_TARGET_KEY = 'xml_target'
    _PROCESS_EVIDENCE_DESCR_KEY = 'process_evidence_description'
    _NAME_KEY = 'name'
    _DOCUMENTATION_KEY = 'documentation'
    _RESOLUTION_KEY = 'resolution'
    _TYPE_KEY = 'type'
    _FIELDS_KEY = 'fields'
    _WIDTH_KEY = 'width'
    _HEIGHT_KEY = 'height'
    # Prefixes
    _SEQUENCE_FLOW_PREFIX = 'SequenceFlow_'
    _START_EVENT_PREFIX = 'StartEvent_'
    _END_EVENT_PREFIX = 'EndEvent_'
    _PR_PREFIX = 'pr_'
    _TASK_PREFIX = 'Task_'
    _SUBTASK_PREFIX = 'Subtask_'
    _PARTICIPANT_PREFIX = 'participant_'
    _EXCLUSIVE_GATEWAY_PREFIX = 'ExclusiveGateway_'
    _C_PREFIX = 'C'
    _TIMER_PREFIX = f'{_TIMER_STR}_'
    # Suffixes
    _WAYPOINT_SUFFIX = 'waypoint'
    _START_EVENT_SUFFIX = 'startEvent'
    _OUTGOING_SUFFIX = 'outgoing'
    _SEQUENCE_FLOW_SUFFIX = 'sequenceFlow'
    _END_EVENT_SUFFIX = 'endEvent'
    _DATA_OBJECT_SUFFIX = 'dataObject'
    _DATA_OBJECT_REFERENCE_SUFFIX = 'dataObjectReference'
    _DATA_OBJECT_PREFIX = 'DataObject_'
    _DATA_OBJECT_REFERENCE_PREFIX = 'DataObjectReference_'
    _INCOMING_SUFFIX = 'incoming'
    _TASK_SUFFIX = 'task'
    _MANUAL_TASK_SUFFIX = 'manualTask'
    _SERVICE_TASK_SUFFIX = 'serviceTask'
    _COLLABORATION_SUFFIX = 'collaboration'
    _PARTICIPANT_SUFFIX = 'participant'
    _EXCLUSIVE_GATEWAY_SUFFIX = 'exclusiveGateway'
    _1_SUFFIX = '_1'
    _LAST_SUFFIX = '_last'
    _DI_SUFFIX = '_di'
    _1_DI_SUFFIX = '_1_di'
    _LAST_DI_SUFFIX = '_last_di'
    _BOUNDS_SUFFIX = 'Bounds'
    _SHAPE_SUFFIX = 'BPMNShape'
    _LABEL_SUFFIX = 'BPMNLabel'
    _EDGE_SUFFIX = 'BPMNEdge'
    _DIAGRAM_SUFFIX = 'BPMNDiagram'
    _PLANE_SUFFIX = 'BPMNPlane'
    _BOUNDARY_EVENT_SUFFIX = 'boundaryEvent'
    _TIMER_EVENT_DEF_SUFFIX = 'timerEventDefinition'
    _TIME_DURATION_SUFFIX = 'timeDuration'
    # Process evidences
    _PROCESS_EVIDENCES_STR = 'Process evidences'
    _PROCESS_EVIDENCE_PREFIX = 'process_evidence'
    # Process steps
    _PROCESS_STEPS_STR = 'Process steps'
    _PROCESS_STEPS_DIGITAL_STR = 'Process steps digital'
    _PROCESS_STEP_PREFIX = 'process_step'
    _PROCESS_STEP_TITLE = None
    _PROCESS_STEP_IMPLEMENTATION = None
    _PROCESS_STEP_CHILD = None
    _PROCESS_STEP_PREVIOUS_CHILD = None

    # Process steps duration
    _PROCESS_STEP_DURATION_MAX = 'process_step_duration_max'
    _PROCESS_STEP_DURATION_MIN = 'process_step_duration_min'
    _PROCESS_STEP_DURATION_TYPE = 'process_step_duration_type'
    _PROCESS_STEP_DIGITAL_DURATION_MAX = 'process_step_digital_duration_max'
    _PROCESS_STEP_DIGITAL_DURATION_MIN = 'process_step_digital_duration_min'
    _PROCESS_STEP_DIGITAL_DURATION_TYPE = 'process_step_digital_duration_type'
    _TFORMALEXPRESSION = 'tFormalExpression'

    _MAX_TEXT_LEN = 120

    # X
    _X_SHAPE = 100
    # Y
    _Y_SHAPE = 218
    _Y_SHAPE_VARIANT = 221
    _Y_SHAPE_VARIANT_2 = 257
    _Y_SHAPE_VARIANT_3 = 111
    _Y_LABEL = 230
    _Y_LABEL_VARIANT = 190
    _Y_START = 240
    # Widths
    _WIDTH_TASK = 100
    _WIDTH_ARROW = 50
    _WIDTH_RHOMBUS = 50
    _WIDTH_START_EVENT = 36
    _WIDTH_LABEL = 90
    _WIDTH_LABEL_VARIANT = 30
    _WIDTH_LABEL_VARIANT_2 = 190
    _WIDTH_DOC_ICON = 36
    # Heights
    _HEIGHT_LABEL = 40
    _HEIGHT_LABEL_VARIANT = _WIDTH_LABEL_VARIANT
    _HEIGHT_RHOMBUS = _WIDTH_RHOMBUS
    _HEIGHT_DOC_ICON = 50
    # Offsets
    _OFFSET_BOUNDS = 15
    _OFFSET_X_LABEL = 19
    _OFFSET_Y_LABEL = 10
    _OFFSET_HANDLE_PLAIN_NODE_SHAPES = 50
    _OFFSET_X_APPEND_SHAPES_AND_EDGES = 150
    _OFFSET_DOC_TAB = 200
    _OFFSET_X_DOC = 100
    _OFFSET_Y_DOC = 150
    _OFFSET_EVIDENCES_Y_LABEL = 60
    _OFFSET_BOUNDS_WIDTH = 50
    # Multipliers
    _MULTIPLIER_BOX_HEIGHT = 120
    _MULTIPLIER_TASK_TEXT_HEIGHT = 27
    _MULTIPLIER_EVIDENCES_TEXT_HEIGHT = 10
    # Divisors
    _DIVISOR_BOX_HEIGHT = 25
    _DIVISOR_TASK_ROWS = 9
    _DIVISOR_EVIDENCES_ROWS = 6.5
    # Iraklis stuff
    # Widths
    _TASK_WIDTH = 100
    _ARROW_WIDTH = 50
    _RHOMBUS_WIDTH = 50
    _START_EVENT_WIDTH = 36
    # HEIGHTS
    _CHAINS_y_OFFSET = 120
    # Namespaces
    _NAMESPACE = {
        _SEMANTIC_KEY: 'http://www.omg.org/spec/BPMN/20100524/MODEL',
        _BPMNDI_KEY: 'http://www.omg.org/spec/BPMN/20100524/DI',
        _DI_KEY: 'http://www.omg.org/spec/DD/20100524/DI',
        _DC_KEY: 'http://www.omg.org/spec/DD/20100524/DC',
        _XSI_KEY: 'http://www.w3.org/2001/XMLSchema-instance',
        _XML_TARGET_KEY: 'http://www.trisotech.com/definitions/_1276276944297'
    }
    # Attributes
    _NAME_ATTR_PREFIX = 'Collaboration Diagram for '
    _DOCUMENTATION_ATTR = ''
    _RESOLUTION_ATTR = '96.00000267028808'
    _TYPE_ATTR = 'dc:Point'
    # Other
    _SEQUENCE_FLOW_0 = 'SequenceFlow_0'
    _START_EVENT_0 = 'StartEvent_0'
    _START_EVENT_NAME = 'Έναρξη'
    _END_EVENT_NAME = 'Λήξη'
    _SELECTION_NAME = 'Επιλογή'
    _MANUAL_TASK = 'Χειροκίνητη ενέργεια'
    _SERVICE_TASK = 'Ενέργεια μέσω λογισμικού'
    _TASK_1_NAME = 'Task_1'
    _PROCESS_STR = 'process'
    _LANE_SET_STR = 'laneSet'
    _IS_EXECUTABLE_FALSE = 'false'
    _IS_MARKER_VISIBLE_TRUE = 'true'
    _IS_HORIZONTAL_TRUE = 'true'
    _PROCESS_STEP_CHILD_YES = 'Ναι'
    _TRISOTECH_ID = 'Trisotech.Visio_'
    _TRISOTECH_ID_6 = 'Trisotech.Visio-_6'
    _PLANE_FIND_ALL_QUERY = './bpmndi:BPMNShape[@isHorizontal]/dc:Bounds'

    def __init__(self, digital_steps=False):
        if digital_steps:
            self._PROCESS_STEPS_STR += f' {_DIGITAL_STR}'
            process_step_prefix = f'{self._PROCESS_STEP_PREFIX}_{_DIGITAL_STR}'
        else:
            process_step_prefix = self._PROCESS_STEP_PREFIX
        self._PROCESS_STEP_TITLE = f'{process_step_prefix}_{_TITLE_STR}'
        self._PROCESS_STEP_IMPLEMENTATION = \
            f'{process_step_prefix}_{_IMPLEMENTATION_STR}'
        self._PROCESS_STEP_NUM_ID = f'{process_step_prefix}_{_NUM_ID_SUFFIX}'
        self._PROCESS_STEP_CHILD = f'{process_step_prefix}_{_CHILD_STR}'
        self._PROCESS_STEP_PREVIOUS_CHILD = f'{process_step_prefix}_'\
            f'{_PREV_CHILD_SUFFIX}'

        self._ns = BPMNNamespaces(
            semantic=bracketize(self._NAMESPACE[self._SEMANTIC_KEY]),
            bpmndi=bracketize(self._NAMESPACE[self._BPMNDI_KEY]),
            di=bracketize(self._NAMESPACE[self._DI_KEY]),
            dc=bracketize(self._NAMESPACE[self._DC_KEY]),
            xsi=bracketize(self._NAMESPACE[self._XSI_KEY]),
            NSMAP=self._NAMESPACE,
            di_NAMESPACE=self._NAMESPACE[self._DI_KEY],
            xsi_NAMESPACE=self._NAMESPACE[self._XSI_KEY])
        self._process_steps = None

    def group_options(self, opts):
        chains = {}
        for n in opts:
            curr = n.get(self._PROCESS_STEP_NUM_ID)
            prev = n.get(self._PROCESS_STEP_PREVIOUS_CHILD)
            if chains.get(prev) is not None:
                arr = chains.get(prev)
                arr.append(n)
                chains.pop(prev, None)
                chains[curr] = arr
            else:
                arr = []
                arr.append(n)
                chains[curr] = arr
        maxchainlength = 0
        for k in chains:
            if len(chains.get(k)) > maxchainlength:
                maxchainlength = len(chains.get(k))
        return chains, maxchainlength

    def _create_waypoint(self, BPMNEdge, x, y):
        waypoint = etree.SubElement(
            BPMNEdge,
            self._ns.di + self._WAYPOINT_SUFFIX,
            x=str(x),
            y=str(y),
            nsmap=self._ns.NSMAP)
        return waypoint

    def _appendStartEventTree(self, process, allnodes):
        """Append the xml elements of the StartEvent"""
        task = etree.SubElement(
            process, self._ns.semantic + self._START_EVENT_SUFFIX,
            id=self._START_EVENT_0,
            name=self._START_EVENT_NAME, nsmap=self._ns.NSMAP)
        outgoing = etree.SubElement(
            task, self._ns.semantic + self._OUTGOING_SUFFIX,
            nsmap=self._ns.NSMAP)
        outgoing.text = self._SEQUENCE_FLOW_0
        allnodes.append(self._START_EVENT_0)

    def _appendDataObjects(self, definition, process, data):
        stepcount = 0
        # Parse the Evidences and add a dataObject and a dataObjectReference
        # for each
        for evidence_num, evidence in self._process_evidences.items():
            stepcount += 1
            evid_name = evidence[self._PROCESS_EVIDENCE_DESCR_KEY]
            if evid_name is not None:
                if len(evid_name) > self._MAX_TEXT_LEN:
                    evid_name = evid_name[:self._MAX_TEXT_LEN] + '...'
                etree.SubElement(
                    process,
                    self._ns.semantic + self._DATA_OBJECT_SUFFIX,
                    id=self._DATA_OBJECT_PREFIX + str(stepcount))
                etree.SubElement(
                    process,
                    self._ns.semantic + self._DATA_OBJECT_REFERENCE_SUFFIX,
                    id=self._DATA_OBJECT_REFERENCE_PREFIX + str(stepcount),
                    name=evid_name,
                    dataObjectRef=self._DATA_OBJECT_PREFIX + str(stepcount))

    def _appendEndNodes(self, data, process, allnodes, options, stepcount):
        """Append the xml elements of the EndEvent"""
        stepname = self._END_EVENT_NAME
        task = etree.SubElement(process,
                                self._ns.semantic + self._END_EVENT_SUFFIX,
                                id=self._END_EVENT_PREFIX + str(stepcount + 1),
                                name=stepname, nsmap=self._ns.NSMAP)
        incoming = etree.SubElement(
            task, self._ns.semantic + self._INCOMING_SUFFIX,
            nsmap=self._ns.NSMAP)
        incoming.text = self._SEQUENCE_FLOW_PREFIX + str(stepcount)
        allnodes.append(self._END_EVENT_PREFIX + str(stepcount + 1))
        task = etree.SubElement(
            process, self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
            id=(self._SEQUENCE_FLOW_PREFIX + str(stepcount)),
            sourceRef=allnodes[stepcount],
            targetRef=allnodes[stepcount + 1],
            nsmap=self._ns.NSMAP)

    def _appendProcessTree(self, definition, process_name, data):
        process = etree.SubElement(definition,
                                   self._ns.semantic + self._PROCESS_STR,
                                   id=self._PR_PREFIX + str(
                                       hash(process_name)),
                                   isExecutable=self._IS_EXECUTABLE_FALSE,
                                   nsmap=self._ns.NSMAP)
        etree.SubElement(process, self._ns.semantic + self._LANE_SET_STR,
                         nsmap=self._ns.NSMAP)
        stepcount = 0
        allnodes = []
        options = []
        branched = False
        # Add a default startEvent
        self._appendStartEventTree(process, allnodes)
        # Parse the process steps and either add plainEventNodes, or
        # put the branchNodes in a list and handle them when the first
        # plainEventNode appears
        for step_num, step in self._process_steps.items():
            stepcount += 1
            if step.get(self._PROCESS_STEP_TITLE) is not None:
                if step.get(self._PROCESS_STEP_CHILD) == \
                        self._PROCESS_STEP_CHILD_YES:
                    options.append(step)
                    allnodes.append(self._SUBTASK_PREFIX + str(stepcount))
                    branched = True
                else:
                    self._handlePlainNodes(data, process, allnodes, options,
                                           stepcount)
                    allnodes.append(self._TASK_PREFIX + str(stepcount))
                    options = []
                    if not branched:
                        etree.SubElement(
                            process,
                            self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
                            id=self._SEQUENCE_FLOW_PREFIX + str(stepcount - 1),
                            sourceRef=allnodes[stepcount - 1],
                            targetRef=allnodes[stepcount],
                            nsmap=self._ns.NSMAP)
                    branched = False
        # Add a default endEvent
        self._appendEndNodes(data, process, allnodes, options, stepcount)
        return process

    def _handlePlainNodes(self, data, process, allnodes, options, stepcount):
        process_steps = self._process_steps
        stepname = process_steps[stepcount].get(self._PROCESS_STEP_TITLE)
        stepimpl = process_steps[stepcount].get(
            self._PROCESS_STEP_IMPLEMENTATION)
        if stepimpl == self._MANUAL_TASK:
            steptype = self._MANUAL_TASK_SUFFIX
        elif stepimpl == self._SERVICE_TASK:
            steptype = self._SERVICE_TASK_SUFFIX
        else:
            steptype = self._TASK_SUFFIX
        step = process_steps[stepcount]
        if self._PROCESS_STEPS_STR == self._PROCESS_STEPS_DIGITAL_STR:
            timer = (step.get(self._PROCESS_STEP_DIGITAL_DURATION_MIN),
                     step.get(self._PROCESS_STEP_DIGITAL_DURATION_MAX),
                     step.get(self._PROCESS_STEP_DIGITAL_DURATION_TYPE))
        else:
            timer = (step.get(self._PROCESS_STEP_DURATION_MIN),
                     step.get(self._PROCESS_STEP_DURATION_MAX),
                     step.get(self._PROCESS_STEP_DURATION_TYPE))

        # If no BranchNodes are found
        if len(options) == 0:
            task = etree.SubElement(process,
                                    self._ns.semantic + steptype,
                                    id=self._TASK_PREFIX + str(stepcount),
                                    name=stepname, nsmap=self._ns.NSMAP)
            name = f'{_MIN_STR}:{timer[0]}, {_MAX_STR}:{timer[1]} {timer[2]}'
            boundaryEvent = etree.SubElement(
                process, self._ns.semantic + self._BOUNDARY_EVENT_SUFFIX,
                id=self._TIMER_PREFIX + str(stepcount),
                # name=self._TASK_PREFIX + str(stepcount)+"_Duration",
                name=name,
                attachedToRef=self._TASK_PREFIX + str(stepcount),
                nsmap=self._ns.NSMAP)
            timerEvent = etree.SubElement(
                boundaryEvent,
                self._ns.semantic + self._TIMER_EVENT_DEF_SUFFIX)
            timeDuration = etree.SubElement(
                timerEvent, self._ns.semantic + self._TIME_DURATION_SUFFIX)
            timeDuration.attrib[
                QName(self._ns.xsi_NAMESPACE, self._TYPE_KEY)] = \
                _SEMANTIC_STR + ':' + self._TFORMALEXPRESSION
            timeDuration.text = getMaxDurationAsString(timer)
            incoming = etree.SubElement(
                task, self._ns.semantic + self._INCOMING_SUFFIX,
                nsmap=self._ns.NSMAP)
            incoming.text = self._SEQUENCE_FLOW_PREFIX + str(stepcount - 1)
            outgoing = etree.SubElement(
                task, self._ns.semantic + self._OUTGOING_SUFFIX,
                nsmap=self._ns.NSMAP)
            outgoing.text = self._SEQUENCE_FLOW_PREFIX + str(stepcount)
        # If there BranchNodes are found from the previous event
        else:
            chains, maxchainlength = self.group_options(options)
            self._addBranchNodes(options, chains, process, allnodes, stepcount)
            self._addMergeNodes(options, chains, process,
                                allnodes, stepcount, stepname,
                                steptype, timer, False)
            options = []

    def _appendCollaboration(self, definition, process_name):
        collaboration = etree.SubElement(
            definition,
            self._ns.semantic + self._COLLABORATION_SUFFIX,
            id=self._C_PREFIX + str(hash(process_name)),
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            collaboration, self._ns.semantic + self._PARTICIPANT_SUFFIX,
            id=self._PARTICIPANT_PREFIX + str(hash(process_name)),
            name=process_name,
            processRef=self._PR_PREFIX + str(hash(process_name)),
            nsmap=self._ns.NSMAP)

    def _addBranchNodes(self, options, chains, process, allnodes, stepcount):
        # Add an exclusiveGateway node, with an incoming edge
        exclusiveGateway = etree.SubElement(
            process, self._ns.semantic + self._EXCLUSIVE_GATEWAY_SUFFIX,
            id=self._EXCLUSIVE_GATEWAY_PREFIX + str(stepcount - len(options)),
            name=self._SELECTION_NAME, nsmap=self._ns.NSMAP)
        incoming = etree.SubElement(
            exclusiveGateway,
            self._ns.semantic + self._INCOMING_SUFFIX,
            nsmap=self._ns.NSMAP)
        incoming.text = self._SEQUENCE_FLOW_PREFIX + \
            str(stepcount - len(options) - 1)
        # Add a sequenceFlow for the exclusiveGateway's incoming edge
        etree.SubElement(
            process, self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
            id=self._SEQUENCE_FLOW_PREFIX + str(stepcount - len(options) - 1),
            sourceRef=allnodes[stepcount - len(options) - 1],
            targetRef=self._EXCLUSIVE_GATEWAY_PREFIX + str(
                stepcount - len(options)),
            nsmap=self._ns.NSMAP)
        # Add:
        # i) the outgoing edges
        # ii) the sequenceFlows for the outgoing edges,
        # iii) the subtasks,
        # iv) their incoming and outgoing edges,
        # v) the respective SequenceFlows for these edges
        subchaincount = 0
        for key in chains:
            chain = chains.get(key)
            subchaincount += 1
            #  Handle the 1st node of each chain
            step = chain[0]

            stepimpl = step.get(self._PROCESS_STEP_IMPLEMENTATION)
            if stepimpl == self._MANUAL_TASK:
                steptype = self._MANUAL_TASK_SUFFIX
            elif stepimpl == self._SERVICE_TASK:
                steptype = self._SERVICE_TASK_SUFFIX
            else:
                steptype = self._TASK_SUFFIX

            if self._PROCESS_STEPS_STR == self._PROCESS_STEPS_DIGITAL_STR:
                timer = (step.get(self._PROCESS_STEP_DIGITAL_DURATION_MIN),
                         step.get(self._PROCESS_STEP_DIGITAL_DURATION_MAX),
                         step.get(self._PROCESS_STEP_DIGITAL_DURATION_TYPE))
            else:
                timer = (step.get(self._PROCESS_STEP_DURATION_MIN),
                         step.get(self._PROCESS_STEP_DURATION_MAX),
                         step.get(self._PROCESS_STEP_DURATION_TYPE))

            # i)
            outgoing = etree.SubElement(
                exclusiveGateway,
                self._ns.semantic + self._OUTGOING_SUFFIX,
                nsmap=self._ns.NSMAP)
            # ii)  e.g. SequenceFlow_3_1_1, SequenceFlow_3_2_1  etc.
            outgoing.text = self._SEQUENCE_FLOW_PREFIX + \
                str(stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX
            # iii) e.g. Subtask_3_1_1, Subtask_3_2_1  etc.
            subTask = etree.SubElement(
                process, self._ns.semantic + steptype,
                id=self._SUBTASK_PREFIX + str(
                    stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX,
                name=step.get(self._PROCESS_STEP_TITLE),
                nsmap=self._ns.NSMAP)
            name = f'{_MIN_STR}:{timer[0]}, {_MAX_STR}:{timer[1]} {timer[2]}'
            boundaryEvent = etree.SubElement(
                process, self._ns.semantic + self._BOUNDARY_EVENT_SUFFIX,
                id=self._TIMER_PREFIX + str(
                    stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX,
                # name=self._TASK_PREFIX + str(stepcount)+"_Duration",
                name=name,
                attachedToRef=self._SUBTASK_PREFIX + str(
                    stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX,
                nsmap=self._ns.NSMAP)
            timerEvent = etree.SubElement(
                boundaryEvent,
                self._ns.semantic + self._TIMER_EVENT_DEF_SUFFIX)
            timeDuration = etree.SubElement(
                timerEvent, self._ns.semantic + self._TIME_DURATION_SUFFIX)
            timeDuration.attrib[
                QName(self._ns.xsi_NAMESPACE, self._TYPE_KEY)] = \
                _SEMANTIC_STR + ':' + self._TFORMALEXPRESSION
            timeDuration.text = getMaxDurationAsString(timer)
            # Set the current task as the last task of the chain
            lastTask = subTask
            # iv)
            incoming = etree.SubElement(
                subTask, self._ns.semantic + self._INCOMING_SUFFIX,
                nsmap=self._ns.NSMAP)
            incoming.text = self._SEQUENCE_FLOW_PREFIX + \
                str(stepcount - len(options)) + '_' + \
                str(subchaincount) + '_' + str(1)
            etree.SubElement(
                process,
                self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
                id=self._SEQUENCE_FLOW_PREFIX + str(
                    stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX,
                sourceRef=self._EXCLUSIVE_GATEWAY_PREFIX + str(
                    stepcount - len(options)),
                targetRef=self._SUBTASK_PREFIX + str(
                    stepcount - len(options)) + '_' + str(
                    subchaincount) + self._1_SUFFIX, nsmap=self._ns.NSMAP)
            if (len(chain) > 1):
                lastTask = self._add_chain_nodes(
                    options, chain, process, stepcount, subchaincount,
                    lastTask)
            # In all cases
            outgoing = etree.SubElement(
                lastTask, self._ns.semantic + self._OUTGOING_SUFFIX,
                nsmap=self._ns.NSMAP)
            outgoing.text = self._SEQUENCE_FLOW_PREFIX + \
                str(stepcount - len(options) + 1) + \
                '_' + str(subchaincount) + self._LAST_SUFFIX

    def _add_chain_nodes(self, options, chain, process, stepcount,
                         subchaincount, lastTask):
        """This method adds the remaining nodes of a chain.
        If the case has more than one step
        """
        substepcount = 1
        for step in chain[1:]:
            stepimpl = step.get(self._PROCESS_STEP_IMPLEMENTATION)
            if stepimpl == self._MANUAL_TASK:
                steptype = self._MANUAL_TASK_SUFFIX
            elif stepimpl == self._SERVICE_TASK:
                steptype = self._SERVICE_TASK_SUFFIX
            else:
                steptype = self._TASK_SUFFIX
            timer = (step.get(self._PROCESS_STEP_DURATION_MIN),
                     step.get(self._PROCESS_STEP_DURATION_MAX),
                     step.get(self._PROCESS_STEP_DURATION_TYPE))
            substepcount += 1
            curr_id = str(stepcount - len(options)) + '_' + \
                str(subchaincount) + '_' + str(substepcount)
            prev_id = str(stepcount - len(options)) + '_' + \
                str(subchaincount) + '_' + str(substepcount - 1)
            subTask = etree.SubElement(
                process, self._ns.semantic + steptype,
                id=self._SUBTASK_PREFIX + curr_id,
                name=step.get(self._PROCESS_STEP_TITLE),
                nsmap=self._ns.NSMAP)
            outgoing = etree.SubElement(
                lastTask, self._ns.semantic + self._OUTGOING_SUFFIX,
                nsmap=self._ns.NSMAP)
            outgoing.text = self._SEQUENCE_FLOW_PREFIX + curr_id
            incoming = etree.SubElement(
                subTask, self._ns.semantic + self._INCOMING_SUFFIX,
                nsmap=self._ns.NSMAP)
            incoming.text = self._SEQUENCE_FLOW_PREFIX + curr_id
            etree.SubElement(process,
                             self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
                             id=self._SEQUENCE_FLOW_PREFIX + curr_id,
                             sourceRef=self._SUBTASK_PREFIX + prev_id,
                             targetRef=self._SUBTASK_PREFIX + curr_id,
                             nsmap=self._ns.NSMAP)
            name = f'{_MIN_STR}:{timer[0]}, {_MAX_STR}:{timer[1]} {timer[2]}'
            boundaryEvent = etree.SubElement(
                process, self._ns.semantic + self._BOUNDARY_EVENT_SUFFIX,
                id=self._TIMER_PREFIX + curr_id,
                # name=self._TASK_PREFIX + str(stepcount)+"_Duration",
                name=name,
                attachedToRef=self._SUBTASK_PREFIX + curr_id,
                nsmap=self._ns.NSMAP)
            timerEvent = etree.SubElement(
                boundaryEvent,
                self._ns.semantic + self._TIMER_EVENT_DEF_SUFFIX)
            timeDuration = etree.SubElement(
                timerEvent, self._ns.semantic + self._TIME_DURATION_SUFFIX)
            timeDuration.attrib[
                QName(self._ns.xsi_NAMESPACE, self._TYPE_KEY)] = \
                _SEMANTIC_STR + ':' + self._TFORMALEXPRESSION
            timeDuration.text = getMaxDurationAsString(timer)
            # Set this task as the lastTask so that it can used in the next
            # iteration as the previous node
            lastTask = subTask
        return lastTask

    def _addMergeNodes(self, options, chains, process, allnodes, stepcount,
                       stepname, steptype, timer, isEndNode):
        task = etree.SubElement(process, self._ns.semantic + steptype,
                                id=self._TASK_PREFIX + str(stepcount),
                                name=stepname, nsmap=self._ns.NSMAP)
        subchaincount = 0
        outgoing = etree.SubElement(
            task, self._ns.semantic + self._OUTGOING_SUFFIX,
            nsmap=self._ns.NSMAP)
        outgoing.text = self._SEQUENCE_FLOW_PREFIX + str(stepcount)
        name = f'{_MIN_STR}:{timer[0]}, {_MAX_STR}:{timer[1]} {timer[2]}'
        boundaryEvent = etree.SubElement(
            process, self._ns.semantic + self._BOUNDARY_EVENT_SUFFIX,
            id=self._TIMER_PREFIX + str(stepcount),
            # name=self._TASK_PREFIX + str(stepcount)+"_Duration",
            name=name,
            attachedToRef=self._TASK_PREFIX + str(stepcount),
            nsmap=self._ns.NSMAP)
        timerEvent = etree.SubElement(
            boundaryEvent, self._ns.semantic + self._TIMER_EVENT_DEF_SUFFIX)
        timeDuration = etree.SubElement(
            timerEvent, self._ns.semantic + self._TIME_DURATION_SUFFIX)
        timeDuration.attrib[
            QName(self._ns.xsi_NAMESPACE, self._TYPE_KEY)] = \
            _SEMANTIC_STR + ':' + self._TFORMALEXPRESSION
        timeDuration.text = getMaxDurationAsString(timer)
        for k in chains:
            chain = chains.get(k)
            subchaincount += 1
            incoming = etree.SubElement(
                task, self._ns.semantic + self._INCOMING_SUFFIX,
                nsmap=self._ns.NSMAP)
            incoming.text = self._SEQUENCE_FLOW_PREFIX + \
                str(stepcount - len(options) + 1) + \
                '_' + str(subchaincount) + self._LAST_SUFFIX
            # Append the SequenceFlows for the merge
            # (but maybe it is better to move it to the merge)
            etree.SubElement(process,
                             self._ns.semantic + self._SEQUENCE_FLOW_SUFFIX,
                             id=self._SEQUENCE_FLOW_PREFIX + str(
                                 stepcount - len(options) + 1) + '_' + str(
                                 subchaincount) + self._LAST_SUFFIX,
                             sourceRef=self._SUBTASK_PREFIX + str(
                                 stepcount - len(options)) + '_' + str(
                                 subchaincount) + '_' + str(len(chain)),
                             targetRef=self._TASK_PREFIX + str(stepcount),
                             nsmap=self._ns.NSMAP)

    def _getChainHeights(self, options, chains, stepcount, offset):
        subchaincount = 0
        chainHeights = []
        for k in chains:
            steps = chains.get(k)
            step = steps[0]
            maxChainHeight = 0
            stepname = step.get(self._PROCESS_STEP_TITLE)
            rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
            rounded_height = int(self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)
            subchaincount += 1
            if len(steps) > 1:
                substepcount = 1
                for step in steps[1:]:
                    substepcount += 1
                    stepname = step.get(self._PROCESS_STEP_TITLE)
                    rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
                    rounded_height = int(
                        self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)
                    if rounded_height > maxChainHeight:
                        maxChainHeight = rounded_height
            else:
                stepname = step.get(self._PROCESS_STEP_TITLE)
                rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
                rounded_height = int(self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)
                maxChainHeight = rounded_height
            chainHeights.append(maxChainHeight)
        return chainHeights

    def _addBranchNodeShapes(self, options, chains, BPMNPlane, stepcount,
                             offset, xcurr):
        chainHeights = self._getChainHeights(
            options, chains, stepcount, offset)
        # Add the ExclusiveGateway shape
        mainflownodes = stepcount - len(options)
        BPMNShape = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
            id=self._EXCLUSIVE_GATEWAY_PREFIX + str(
                mainflownodes) + self._DI_SUFFIX,
            bpmnElement=self._EXCLUSIVE_GATEWAY_PREFIX + str(mainflownodes),
            isMarkerVisible=self._IS_MARKER_VISIBLE_TRUE, nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(xcurr + self._WIDTH_ARROW),
            y=str(self._Y_SHAPE),
            width=str(self._WIDTH_RHOMBUS),
            height=str(self._HEIGHT_RHOMBUS),
            nsmap=self._ns.NSMAP)
        BPMNLabel = etree.SubElement(
            BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                         x=str(xcurr + self._OFFSET_BOUNDS),
                         y=str(self._Y_LABEL_VARIANT),
                         width=str(self._WIDTH_LABEL_VARIANT),
                         height=str(self._HEIGHT_LABEL_VARIANT),
                         nsmap=self._ns.NSMAP)
        # Check if the previous flow node is the StartEvent or a simple Task
        head = self._TASK_PREFIX
        if stepcount == 2:
            head = self._START_EVENT_PREFIX
        # Draw an edge from the previous node to the ExclusiveGateway node
        BPMNEdge = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
            id=self._SEQUENCE_FLOW_PREFIX + str(
                mainflownodes - 1) + self._DI_SUFFIX,
            bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(mainflownodes - 1),
            sourceElement=head + str(stepcount - 1 - len(options)),
            targetElement=self._EXCLUSIVE_GATEWAY_PREFIX + str(mainflownodes),
            nsmap=self._ns.NSMAP)
        self._create_waypoint(
            BPMNEdge, xcurr, self._Y_START)
        self._create_waypoint(
            BPMNEdge, xcurr + self._WIDTH_ARROW, self._Y_START)

        BPMNLabel = etree.SubElement(
            BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(round((xcurr + self._WIDTH_ARROW) / 2)),
            y=str(self._Y_LABEL),
            width=str(self._WIDTH_LABEL),
            height=str(self._HEIGHT_LABEL),
            nsmap=self._ns.NSMAP)
        subchaincount = 0
        ymove = 0
        xstart = xcurr + self._WIDTH_ARROW + self._WIDTH_RHOMBUS
        xend = xstart + self._WIDTH_ARROW
        maxChainLength = 1
        gap = self._HEIGHT_LABEL

        for k in chains:
            steps = chains.get(k)
            step = steps[0]
            subchaincount += 1
            stepname = step.get(self._PROCESS_STEP_TITLE)
            rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
            rounded_height = int(self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)
            BPMNShape = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                id=self._SUBTASK_PREFIX + str(
                    mainflownodes) + '_' + str(
                    subchaincount) + self._1_DI_SUFFIX,
                bpmnElement=self._SUBTASK_PREFIX + str(
                    mainflownodes) + '_' + str(subchaincount) + self._1_SUFFIX,
                nsmap=self._ns.NSMAP)
            etree.SubElement(
                BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                x=str(xend),
                y=str(self._Y_SHAPE + ymove),
                width=str(self._WIDTH_TASK),
                # height=str(round(
                #   self._MULTIPLIER_BOX_HEIGHT * boxHeightFactor)),
                height=str(rounded_height),
                nsmap=self._ns.NSMAP)
            if subchaincount == 1:
                BPMNEdge = etree.SubElement(
                    BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
                    id=self._SEQUENCE_FLOW_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_DI_SUFFIX,
                    bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_SUFFIX,
                    sourceElement=self._EXCLUSIVE_GATEWAY_PREFIX + str(
                        mainflownodes),
                    targetElement=self._SUBTASK_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_SUFFIX,
                    nsmap=self._ns.NSMAP)
                self._create_waypoint(BPMNEdge, xstart, self._Y_START)
                self._create_waypoint(BPMNEdge, xend, self._Y_START)
                BPMNLabel = etree.SubElement(
                    BPMNEdge,
                    self._ns.bpmndi + self._LABEL_SUFFIX,
                    nsmap=self._ns.NSMAP)
                etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                                 x=str(round(xend + xstart / 2)),
                                 y=str(self._Y_LABEL),
                                 width=str(self._WIDTH_LABEL),
                                 height=str(self._HEIGHT_LABEL),
                                 nsmap=self._ns.NSMAP)

                # add timer icon in first branched node first line
                id_ = f'{self._SUBTASK_PREFIX}{mainflownodes}_' \
                      f'{subchaincount}_1_Timer_{mainflownodes}_' \
                      f'{subchaincount}_1'
                BPMNShape = etree.SubElement(
                    BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                    id=id_,
                    bpmnElement=self._TIMER_PREFIX + str(
                        mainflownodes) + '_' + str(subchaincount) + '_1',
                    nsmap=self._ns.NSMAP)
                etree.SubElement(
                    BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(xend + self._TASK_WIDTH - 15),
                    y=str(self._Y_LABEL_VARIANT + rounded_height + 10),
                    width=str(32),
                    height=str(32),
                    nsmap=self._ns.NSMAP)
                BPMNLabel = etree.SubElement(
                    BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                    nsmap=self._ns.NSMAP)
                y = str(self._Y_LABEL_VARIANT + rounded_height + 32)
                etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                                 x=str(xend + 30),
                                 y=y,
                                 width=str(self._WIDTH_LABEL_VARIANT),
                                 height=str(self._HEIGHT_LABEL_VARIANT),
                                 nsmap=self._ns.NSMAP)
            else:
                BPMNEdge = etree.SubElement(
                    BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
                    id=self._SEQUENCE_FLOW_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_DI_SUFFIX,
                    bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_SUFFIX,
                    sourceElement=self._EXCLUSIVE_GATEWAY_PREFIX + str(
                        mainflownodes),
                    targetElement=self._SUBTASK_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + self._1_SUFFIX,
                    nsmap=self._ns.NSMAP)
                self._create_waypoint(BPMNEdge, xstart, self._Y_START)
                self._create_waypoint(
                    BPMNEdge, xstart, self._Y_START + ymove)
                self._create_waypoint(
                    BPMNEdge, xend, self._Y_START + ymove)
                BPMNLabel = etree.SubElement(
                    BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
                    nsmap=self._ns.NSMAP)
                etree.SubElement(
                    BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(round(xend + xstart / 2)),
                    y=str(self._Y_LABEL + ymove),
                    width=str(self._WIDTH_LABEL),
                    height=str(self._HEIGHT_LABEL),
                    nsmap=self._ns.NSMAP)

                # add timer icon in first branched node all other lines
                BPMNShape = etree.SubElement(
                    BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                    id=self._SUBTASK_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + '_1' + '_Timer_' + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + '_1',
                    bpmnElement=self._TIMER_PREFIX + str(
                        mainflownodes) + '_' + str(
                        subchaincount) + '_1',
                    nsmap=self._ns.NSMAP)
                etree.SubElement(
                    BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(xend + self._TASK_WIDTH - 15),
                    y=str(ymove + self._Y_LABEL_VARIANT + rounded_height + 10),
                    width=str(32),
                    height=str(32),
                    nsmap=self._ns.NSMAP)
                BPMNLabel = etree.SubElement(
                    BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                    nsmap=self._ns.NSMAP)
                y = str(ymove + self._Y_LABEL_VARIANT + rounded_height + 32)
                etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                                 x=str(xend + 30),
                                 y=y,
                                 width=str(self._WIDTH_LABEL_VARIANT),
                                 height=str(self._HEIGHT_LABEL_VARIANT),
                                 nsmap=self._ns.NSMAP)

            if len(steps) > 1:
                if len(steps) > maxChainLength:
                    maxChainLength = len(steps)
                substepcount = 1
                for step in steps[1:]:
                    substepcount += 1

                    stepname = step.get(self._PROCESS_STEP_TITLE)
                    rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
                    rounded_height = int(
                        self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)

                    curr_subtask_id = str(mainflownodes) + '_' + \
                        str(subchaincount) + '_' + str(substepcount)
                    prev_subtask_id = str(mainflownodes) + '_' + \
                        str(subchaincount) + '_' + str(substepcount - 1)
                    bpmn_shape_id = self._SUBTASK_PREFIX + curr_subtask_id + \
                        self._DI_SUFFIX
                    BPMNShape = etree.SubElement(
                        BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                        id=bpmn_shape_id,
                        bpmnElement=self._SUBTASK_PREFIX + curr_subtask_id,
                        nsmap=self._ns.NSMAP)
                    xnew = xstart + (self._WIDTH_ARROW + self._WIDTH_TASK) * \
                        (substepcount - 1)
                    xcurr = xnew + (self._WIDTH_ARROW + self._WIDTH_TASK)

                    etree.SubElement(
                        BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                        x=str(xnew + self._WIDTH_ARROW),
                        y=str(self._Y_SHAPE + ymove),
                        width=str(self._WIDTH_TASK),
                        height=str(rounded_height),
                        nsmap=self._ns.NSMAP)
                    bpmn_edge_id = self._SEQUENCE_FLOW_PREFIX + \
                        curr_subtask_id + self._DI_SUFFIX
                    bpmn_element = self._SEQUENCE_FLOW_PREFIX + curr_subtask_id
                    BPMNEdge = etree.SubElement(
                        BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
                        id=bpmn_edge_id,
                        bpmnElement=bpmn_element,
                        sourceElement=self._SUBTASK_PREFIX + prev_subtask_id,
                        targetElement=self._SUBTASK_PREFIX + curr_subtask_id,
                        nsmap=self._ns.NSMAP)
                    self._create_waypoint(
                        BPMNEdge, xnew, self._Y_START + ymove)
                    self._create_waypoint(
                        BPMNEdge, xnew + self._WIDTH_ARROW,
                        self._Y_START + ymove)
                    BPMNLabel = etree.SubElement(
                        BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
                        nsmap=self._ns.NSMAP)
                    etree.SubElement(
                        BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                        x=str(round(xnew + self._WIDTH_ARROW / 2)),
                        y=str(self._Y_LABEL),
                        width=str(self._WIDTH_LABEL),
                        height=str(self._HEIGHT_LABEL),
                        nsmap=self._ns.NSMAP)
                    # Add timer icon in branched nodes
                    id_ = f'{self._SUBTASK_PREFIX}{curr_subtask_id}_Timer_' \
                          f'{curr_subtask_id}'
                    BPMNShape = etree.SubElement(
                        BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                        id=id_,
                        bpmnElement=self._TIMER_PREFIX + curr_subtask_id,
                        nsmap=self._ns.NSMAP)
                    y = str(
                        ymove + self._Y_LABEL_VARIANT + rounded_height + 10)
                    etree.SubElement(
                        BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                        x=str(xcurr - 15),
                        y=y,
                        width=str(32),
                        height=str(32),
                        nsmap=self._ns.NSMAP)
                    BPMNLabel = etree.SubElement(
                        BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                        nsmap=self._ns.NSMAP)
                    y = str(
                        ymove + self._Y_LABEL_VARIANT + rounded_height + 32)
                    etree.SubElement(BPMNLabel,
                                     self._ns.dc + self._BOUNDS_SUFFIX,
                                     x=str(xcurr - self._TASK_WIDTH + 30),
                                     y=y,
                                     width=str(self._WIDTH_LABEL_VARIANT),
                                     height=str(self._HEIGHT_LABEL_VARIANT),
                                     nsmap=self._ns.NSMAP)

            ymove += gap + chainHeights[subchaincount - 1]
        return ymove, chainHeights

    def _addMergeNodeShapes(self, options, chains, BPMNPlane, stepcount,
                            offset, chainHeights, maxBranchLength, xcurr,
                            rounded_height):
        gap = self._HEIGHT_LABEL
        head = self._TASK_PREFIX
        xstart = xcurr + self._WIDTH_ARROW + \
            self._WIDTH_RHOMBUS + (self._WIDTH_ARROW + self._WIDTH_TASK)
        mainflownodes = stepcount - len(options)
        ymove = 0
        subchaincount = 0
        for k in chains:
            chain = chains.get(k)
            subchaincount += 1
            currstart = xstart + (len(chain) - 1) * \
                (self._WIDTH_ARROW + self._WIDTH_TASK)
            xend = xstart + (maxBranchLength - 1) * \
                (self._WIDTH_ARROW + self._WIDTH_TASK) + 2 * self._WIDTH_ARROW
            xmid = round((currstart + xend) / 2)
            BPMNEdge = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
                id=self._SEQUENCE_FLOW_PREFIX + str(
                    mainflownodes + 1) + '_' + str(
                    subchaincount) + self._LAST_DI_SUFFIX,
                bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(
                    mainflownodes + 1) + '_' + str(
                    subchaincount) + self._LAST_SUFFIX,
                sourceElement=self._SUBTASK_PREFIX + str(
                    mainflownodes) + '_' + str(
                    subchaincount) + '_' + str(len(chain)),
                targetElement=head + str(stepcount),
                nsmap=self._ns.NSMAP)
            BPMNLabel = etree.SubElement(BPMNEdge,
                                         self._ns.bpmndi + self._LABEL_SUFFIX,
                                         nsmap=self._ns.NSMAP)
            if subchaincount == 1:
                self._create_waypoint(BPMNEdge, currstart, self._Y_START)
                self._create_waypoint(BPMNEdge, xend, self._Y_START)
                etree.SubElement(
                    BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(xmid),
                    y=str(self._Y_LABEL),
                    width=str(self._WIDTH_LABEL),
                    height=str(self._HEIGHT_LABEL),
                    nsmap=self._ns.NSMAP)
            else:
                self._create_waypoint(
                    BPMNEdge, currstart, self._Y_START + ymove)
                self._create_waypoint(
                    BPMNEdge, xmid, self._Y_START + ymove)
                self._create_waypoint(BPMNEdge, xmid, self._Y_START)
                self._create_waypoint(BPMNEdge, xend, self._Y_START)
                etree.SubElement(
                    BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(xmid),
                    y=str(self._Y_START + ymove - self._OFFSET_Y_LABEL),
                    width=str(self._WIDTH_LABEL),
                    height=str(self._HEIGHT_LABEL),
                    nsmap=self._ns.NSMAP)
            ymove += gap + chainHeights[subchaincount - 1]
        BPMNShape = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
            id=head + str(stepcount) + self._DI_SUFFIX,
            bpmnElement=head + str(stepcount),
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(xend),
            y=str(self._Y_START - self._OFFSET_BOUNDS),
            width=str(self._WIDTH_TASK),
            height=str(rounded_height),
            nsmap=self._ns.NSMAP)

    def _handlePlainNodeShapes(self, data, options, BPMNPlane, stepcount,
                               offset, planeHeight, xcurr, rounded_height):
        if len(options) == 0:
            BPMNShape = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                id=self._TASK_PREFIX + str(stepcount) + self._DI_SUFFIX,
                bpmnElement=self._TASK_PREFIX + str(stepcount),
                nsmap=self._ns.NSMAP)
            etree.SubElement(
                BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                x=str(xcurr + self._WIDTH_ARROW),
                y=str(self._Y_SHAPE_VARIANT),
                width=str(self._WIDTH_TASK),
                height=str(rounded_height),
                nsmap=self._ns.NSMAP)
            new_plane_height = rounded_height
            if planeHeight < new_plane_height:
                planeHeight = new_plane_height
            head = self._TASK_PREFIX
            BPMNEdge = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
                id=self._SEQUENCE_FLOW_PREFIX + str(
                    stepcount - 1) + self._DI_SUFFIX,
                bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(stepcount - 1),
                sourceElement=head + str(stepcount - 1),
                targetElement=self._TASK_PREFIX + str(stepcount),
                nsmap=self._ns.NSMAP)
            self._create_waypoint(
                BPMNEdge, xcurr, self._Y_START)
            self._create_waypoint(
                BPMNEdge, xcurr + self._WIDTH_ARROW, self._Y_START)
            BPMNLabel = etree.SubElement(
                BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
                nsmap=self._ns.NSMAP)
            etree.SubElement(
                BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                x=str(round((xcurr + self._WIDTH_ARROW) / 2)),
                y=str(self._Y_LABEL),
                width=str(self._WIDTH_LABEL),
                height=str(self._HEIGHT_LABEL),
                nsmap=self._ns.NSMAP)
            maxchainlength = 0
            xcurr += self._WIDTH_ARROW + self._WIDTH_TASK

            # add timer icon main flow
            BPMNShape = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                id=f'{self._TASK_PREFIX}{stepcount}_Timer_{stepcount}',
                bpmnElement=self._TIMER_PREFIX + str(stepcount),
                nsmap=self._ns.NSMAP)
            etree.SubElement(
                BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                x=str(xcurr - 15),
                y=str(self._Y_LABEL_VARIANT + rounded_height + 10),
                width=str(32),
                height=str(32),
                nsmap=self._ns.NSMAP)
            BPMNLabel = etree.SubElement(
                BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                nsmap=self._ns.NSMAP)
            y = str(self._Y_LABEL_VARIANT + rounded_height + 32)
            etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                             x=str(xcurr - self._TASK_WIDTH + 30),
                             y=y,
                             width=str(self._WIDTH_LABEL_VARIANT),
                             height=str(self._HEIGHT_LABEL_VARIANT),
                             nsmap=self._ns.NSMAP)

        else:
            chains, maxchainlength = self.group_options(options)
            branchHeight, chainHeights = self._addBranchNodeShapes(
                options, chains, BPMNPlane, stepcount, offset, xcurr)
            self._addMergeNodeShapes(options, chains, BPMNPlane, stepcount,
                                     offset, chainHeights,
                                     maxchainlength, xcurr, rounded_height)
            xcurr += (maxchainlength + 1) * self._WIDTH_TASK + \
                (maxchainlength + 3) * self._WIDTH_ARROW + self._WIDTH_RHOMBUS
            if planeHeight < branchHeight:
                planeHeight = branchHeight

            # Add timer icon main flow after branch
            BPMNShape = etree.SubElement(
                BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                id=f'{self._TASK_PREFIX}{stepcount}_Timer_{stepcount}',
                bpmnElement=self._TIMER_PREFIX + str(stepcount),
                nsmap=self._ns.NSMAP)
            etree.SubElement(
                BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                x=str(xcurr - 15),
                y=str(self._Y_LABEL_VARIANT + rounded_height + 10),
                width=str(32),
                height=str(32),
                nsmap=self._ns.NSMAP)
            BPMNLabel = etree.SubElement(
                BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                nsmap=self._ns.NSMAP)
            y = str(self._Y_LABEL_VARIANT + rounded_height + 32)
            etree.SubElement(BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                             x=str(xcurr - self._TASK_WIDTH + 30),
                             y=y,
                             width=str(self._WIDTH_LABEL_VARIANT),
                             height=str(self._HEIGHT_LABEL_VARIANT),
                             nsmap=self._ns.NSMAP)

        return planeHeight, maxchainlength, xcurr

    def _appendShapesAndEdges(self, BPMNPlane, data):
        stepcount = 0
        planeHeight = 0
        totalchainlength = 0
        options = []
        allnodes = []
        xcurr = self._OFFSET_X_APPEND_SHAPES_AND_EDGES
        BPMNShape = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
            id=self._START_EVENT_0 + self._DI_SUFFIX,
            bpmnElement=self._START_EVENT_0, nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(xcurr),
            y=str(self._Y_SHAPE_VARIANT),
            width=str(self._WIDTH_START_EVENT),
            height=str(self._WIDTH_START_EVENT),
            nsmap=self._ns.NSMAP)
        BPMNLabel = etree.SubElement(
            BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(xcurr - self._OFFSET_X_LABEL),
            y=str(self._Y_SHAPE_VARIANT_2),
            width=str(self._WIDTH_LABEL),
            height=str(self._HEIGHT_LABEL),
            nsmap=self._ns.NSMAP)
        BPMNEdge = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
            id=self._SEQUENCE_FLOW_0 + self._DI_SUFFIX,
            bpmnElement=self._SEQUENCE_FLOW_0,
            sourceElement=self._START_EVENT_0,
            targetElement=self._TASK_1_NAME,
            nsmap=self._ns.NSMAP)
        self._create_waypoint(
            BPMNEdge, xcurr + self._WIDTH_START_EVENT, self._Y_START)
        self._create_waypoint(
            BPMNEdge, xcurr + self._WIDTH_START_EVENT + self._WIDTH_ARROW,
            self._Y_START)
        BPMNLabel = etree.SubElement(
            BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(stepcount * self._WIDTH_TASK),
            y=str(self._Y_LABEL),
            width=str(self._WIDTH_LABEL),
            height=str(self._HEIGHT_LABEL),
            nsmap=self._ns.NSMAP)
        xcurr += self._WIDTH_START_EVENT + self._WIDTH_ARROW
        rounded_heights = []
        timers = []
        for step_num, step in self._process_steps.items():
            stepcount += 1
            if step.get(self._PROCESS_STEP_TITLE) is not None:
                if self._PROCESS_STEPS_STR == self._PROCESS_STEPS_DIGITAL_STR:
                    timer = (
                        step.get(self._PROCESS_STEP_DIGITAL_DURATION_MIN),
                        step.get(self._PROCESS_STEP_DIGITAL_DURATION_MAX),
                        step.get(self._PROCESS_STEP_DIGITAL_DURATION_TYPE))
                else:
                    timer = (step.get(self._PROCESS_STEP_DURATION_MIN),
                             step.get(self._PROCESS_STEP_DURATION_MAX),
                             step.get(self._PROCESS_STEP_DURATION_TYPE))

                timers.append(timer)
                stepname = step.get(self._PROCESS_STEP_TITLE)
                rows = int(len(stepname) / self._DIVISOR_TASK_ROWS)
                rounded_height = int(self._MULTIPLIER_TASK_TEXT_HEIGHT * rows)
                rounded_heights.append(rounded_height)

                if stepcount == 1:
                    BPMNShape = etree.SubElement(
                        BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                        id=self._TASK_PREFIX + str(
                            stepcount) + self._DI_SUFFIX,
                        bpmnElement=self._TASK_PREFIX + str(stepcount),
                        nsmap=self._ns.NSMAP)
                    etree.SubElement(
                        BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                        x=str(xcurr),
                        y=str(self._Y_SHAPE_VARIANT),
                        width=str(self._WIDTH_TASK),
                        height=str(rounded_height),
                        nsmap=self._ns.NSMAP)
                    allnodes.append(self._TASK_PREFIX + str(stepcount))
                    xcurr += self._WIDTH_TASK

                    # add timer icon
                    id_ = f'{self._TASK_PREFIX}{stepcount}_Timer_{stepcount}'
                    BPMNShape = etree.SubElement(
                        BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                        id=id_,
                        bpmnElement=self._TIMER_PREFIX + str(stepcount),
                        nsmap=self._ns.NSMAP)
                    etree.SubElement(
                        BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                        x=str(xcurr - 15),
                        y=str(self._Y_LABEL_VARIANT + rounded_height + 10),
                        width=str(32),
                        height=str(32),
                        nsmap=self._ns.NSMAP)
                    BPMNLabel = etree.SubElement(
                        BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                        nsmap=self._ns.NSMAP)
                    y = str(
                        self._Y_LABEL_VARIANT + rounded_height + 32)
                    etree.SubElement(BPMNLabel,
                                     self._ns.dc + self._BOUNDS_SUFFIX,
                                     x=str(xcurr - self._TASK_WIDTH + 30),
                                     y=y,
                                     width=str(self._WIDTH_LABEL_VARIANT),
                                     height=str(self._HEIGHT_LABEL_VARIANT),
                                     nsmap=self._ns.NSMAP)

                else:
                    if step.get(self._PROCESS_STEP_CHILD) == \
                            self._PROCESS_STEP_CHILD_YES:
                        options.append(step)
                    else:
                        planeHeight, maxBranchLength, xcurr = \
                            self._handlePlainNodeShapes(
                                data, options, BPMNPlane, stepcount,
                                self._OFFSET_HANDLE_PLAIN_NODE_SHAPES,
                                planeHeight, xcurr, rounded_height)
                        options = []
                        totalchainlength += maxBranchLength
                        allnodes.append(self._TASK_PREFIX + str(stepcount))
        BPMNShape = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
            id=self._END_EVENT_PREFIX + str(stepcount + 1) + self._DI_SUFFIX,
            bpmnElement=self._END_EVENT_PREFIX + str(stepcount + 1),
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(xcurr + self._WIDTH_ARROW),
            y=str(self._Y_SHAPE_VARIANT), width=str(self._WIDTH_START_EVENT),
            height=str(self._WIDTH_START_EVENT), nsmap=self._ns.NSMAP)
        BPMNLabel = etree.SubElement(
            BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(round(xcurr + self._WIDTH_ARROW / 2)),
            y=str(self._Y_SHAPE_VARIANT_2),
            width=str(self._WIDTH_LABEL),
            height=str(self._HEIGHT_LABEL),
            nsmap=self._ns.NSMAP)
        BPMNEdge = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._EDGE_SUFFIX,
            id=self._SEQUENCE_FLOW_PREFIX + str(stepcount) + self._DI_SUFFIX,
            bpmnElement=self._SEQUENCE_FLOW_PREFIX + str(stepcount),
            sourceElement=self._TASK_PREFIX + str(stepcount),
            targetElement=self._END_EVENT_PREFIX + str(stepcount + 1),
            nsmap=self._ns.NSMAP)
        self._create_waypoint(
            BPMNEdge, xcurr, self._Y_START)
        self._create_waypoint(
            BPMNEdge, xcurr + self._WIDTH_ARROW, self._Y_START)
        BPMNLabel = etree.SubElement(
            BPMNEdge, self._ns.bpmndi + self._LABEL_SUFFIX,
            nsmap=self._ns.NSMAP)
        etree.SubElement(
            BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(round(xcurr + self._WIDTH_ARROW / 2)),
            y=str(self._Y_LABEL),
            width=str(self._WIDTH_LABEL),
            height=str(self._HEIGHT_LABEL),
            nsmap=self._ns.NSMAP)
        return planeHeight + self._OFFSET_X_APPEND_SHAPES_AND_EDGES, xcurr

    def _appendDataObjectShapes(self, BPMNPlane, process_name, planeHeight,
                                boundsWidth):
        # Now add the shapes for the Evidences
        stepcount = 0
        maxtextheight = 0
        xcurr = self._OFFSET_X_DOC
        doc_y = planeHeight + self._OFFSET_Y_DOC
        evidenceRowsStarts = [doc_y]
        maxEvidencesPerRow = int(int(boundsWidth) / self._OFFSET_DOC_TAB)
        for evidence_num, evidence in self._process_evidences.items():
            stepcount += 1
            evid_name = evidence[self._PROCESS_EVIDENCE_DESCR_KEY]
            if evid_name is not None:
                # Calculate rows of the description
                chars = len(evid_name)
                divisor = int(
                    self._WIDTH_LABEL_VARIANT_2 / self._DIVISOR_EVIDENCES_ROWS)
                rows = int(chars / divisor)
                textheight = int(rows * self._MULTIPLIER_EVIDENCES_TEXT_HEIGHT)
                if textheight > maxtextheight:
                    maxtextheight = textheight
                BPMNShape = etree.SubElement(
                    BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
                    id=self._DATA_OBJECT_REFERENCE_PREFIX + str(
                        stepcount) + self._DI_SUFFIX,
                    bpmnElement=self._DATA_OBJECT_REFERENCE_PREFIX + str(
                        stepcount),
                    nsmap=self._ns.NSMAP)
                etree.SubElement(
                    BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(xcurr + self._OFFSET_DOC_TAB),
                    y=str(evidenceRowsStarts[-1]),
                    width=str(self._WIDTH_DOC_ICON),
                    height=str(self._HEIGHT_DOC_ICON), nsmap=self._ns.NSMAP)
                BPMNLabel = etree.SubElement(
                    BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,
                    nsmap=self._ns.NSMAP)
                y = str(
                    evidenceRowsStarts[-1] + self._OFFSET_EVIDENCES_Y_LABEL)
                etree.SubElement(
                    BPMNLabel, self._ns.dc + self._BOUNDS_SUFFIX,
                    x=str(round(xcurr + self._OFFSET_DOC_TAB / 2)),
                    y=y,
                    width=str(self._WIDTH_LABEL_VARIANT_2),
                    height=str(self._HEIGHT_LABEL),
                    nsmap=self._ns.NSMAP)
                xcurr += self._OFFSET_DOC_TAB
            # row is full no so continue to next row
            if stepcount % maxEvidencesPerRow == 0:
                evidence_rows_start = evidenceRowsStarts[-1] + \
                    maxtextheight + 2 * self._HEIGHT_DOC_ICON + \
                    self._OFFSET_X_DOC
                evidenceRowsStarts.append(evidence_rows_start)
                maxtextheight = 0
                xcurr = self._OFFSET_X_DOC
        return xcurr, evidenceRowsStarts[-1] + maxtextheight

    def _appendFlow(self, definition, process_name, data):
        # Add the generic shapes (the flow etc.)
        BPMNDiagram = etree.SubElement(
            definition, self._ns.bpmndi + self._DIAGRAM_SUFFIX,
            id=self._TRISOTECH_ID_6,
            nsmap=self._ns.NSMAP)
        BPMNDiagram.attrib[QName(self._ns.di_NAMESPACE, self._NAME_KEY)
                           ] = self._NAME_ATTR_PREFIX + process_name
        BPMNDiagram.attrib[QName(
            self._ns.di_NAMESPACE,
            self._DOCUMENTATION_KEY)] = self._DOCUMENTATION_ATTR
        BPMNDiagram.attrib[QName(self._ns.di_NAMESPACE, self._RESOLUTION_KEY)
                           ] = self._RESOLUTION_ATTR
        BPMNDiagram.attrib[QName(
            self._ns.xsi_NAMESPACE, self._TYPE_KEY)] = self._TYPE_ATTR
        BPMNPlane = etree.SubElement(
            BPMNDiagram, self._ns.bpmndi + self._PLANE_SUFFIX,
            bpmnElement=self._C_PREFIX + str(hash(process_name)),
            nsmap=self._ns.NSMAP)
        BPMNShape = etree.SubElement(
            BPMNPlane, self._ns.bpmndi + self._SHAPE_SUFFIX,
            id=self._TRISOTECH_ID + self._PARTICIPANT_PREFIX + str(
                hash(process_name)),
            bpmnElement=self._PARTICIPANT_PREFIX + str(hash(process_name)),
            isHorizontal=self._IS_HORIZONTAL_TRUE,
            nsmap=self._ns.NSMAP)
        # Now add the shapes for the main flow
        planeHeight, xcurr = self._appendShapesAndEdges(BPMNPlane, data)
        bounds_width = xcurr + self._WIDTH_START_EVENT + self._WIDTH_ARROW + \
            self._OFFSET_BOUNDS_WIDTH
        etree.SubElement(
            BPMNShape, self._ns.dc + self._BOUNDS_SUFFIX,
            x=str(self._X_SHAPE),
            y=str(self._Y_SHAPE_VARIANT_3),
            width=str(bounds_width),
            height=str(planeHeight),
            nsmap=self._ns.NSMAP)
        etree.SubElement(BPMNShape, self._ns.bpmndi + self._LABEL_SUFFIX,)
        return BPMNPlane, planeHeight, bounds_width

    def xml(self, data):
        """Return the bpmn XML of a service.

        Args:
            data (dict): The data of a service, as returned by
                diavlos.src.service.Service._service_dict().

        Returns:
            str: The bpmn XML representation of a service.
        """
        root = etree.Element(self._DEFINITIONS_KEY)
        root.append(etree.Element(self._PROCESS_STR))
        semanticEl = etree.Element(
            f'{{{self._NAMESPACE[self._SEMANTIC_KEY]}}}{_SEMANTIC_STR}')
        etree.SubElement(
            semanticEl,
            f'{{{self._NAMESPACE[self._SEMANTIC_KEY]}}}{_DEFINITIONS_STR}')
        process_name = data.get(self._NAME_KEY)
        definition = etree.Element(
            self._ns.semantic + self._DEFINITIONS_KEY,
            id='_' + str(hash(process_name)),
            targetNamespace=self._NAMESPACE[self._XML_TARGET_KEY],
            nsmap=self._ns.NSMAP)
        # Read steps in order
        self._process_steps = {}
        stringsorted = data.get(self._FIELDS_KEY).get(self._PROCESS_STEPS_STR)
        if stringsorted is not None:
            integerkeyed = {int(k): v for k, v in stringsorted.items()}
            self._process_steps = OrderedDict(
                sorted(integerkeyed.items(), key=lambda t: t[0]))
        # Read evidences in order
        self._process_evidences = {}
        ev_stringsorted = data.get(self._FIELDS_KEY).get(
            self._PROCESS_EVIDENCES_STR)
        if ev_stringsorted is not None:
            ev_integerkeyed = {int(k): v for k, v in ev_stringsorted.items()}
            self._process_evidences = OrderedDict(
                sorted(ev_integerkeyed.items(), key=lambda t: t[0]))
        if self._process_steps is None:
            xml_string = ''
        else:
            process = self._appendProcessTree(definition, process_name, data)
            self._appendDataObjects(definition, process, data)
            self._appendCollaboration(definition, process_name)
            BPMNPlane, planeHeight, boundsWidth = self._appendFlow(
                definition, process_name, data)
            xdocmax, ydocmax = self._appendDataObjectShapes(
                BPMNPlane, process_name, planeHeight, boundsWidth)
            bs = BPMNPlane.findall(
                self._PLANE_FIND_ALL_QUERY, self._NAMESPACE)
            for b in bs:
                b.attrib[self._WIDTH_KEY] = str(
                    max(xdocmax, int(b.attrib[self._WIDTH_KEY])))
                b.attrib[self._HEIGHT_KEY] = str(ydocmax)
            dom = xml.dom.minidom.parseString(etree.tostring(
                definition, encoding='UTF-8', xml_declaration=True))
            xml_string = dom.toprettyxml()
        return xml_string


import random
import csv
import time
import sys

def life_sim(population_size=100, random_seed=None):
    if random_seed is not None:
        random.seed(random_seed)
    probability = []
    counter = []
    for i in range(101):
        probability.append(1)
        counter.append(0)
    with open('Table01.csv', newline='') as data:
        reader = csv.reader(data)
        first = False
        for row in reader:
            if first:
                probability[int(row[0])] = float(row[1])
            else:
                first = True
    population = []
    result = []
    total = 0
    c = 0
    pop = population_size
    for i in range(population_size):
        pct = int((c/pop)*100)
        print('Complete %d%%' % pct, end='\r')
        sys.stdout.flush()
        population.append(Person(probability))
        k = population[i].sim_life()
        result.append(k)
        total += k
        c += 1
        counter[population[i].age] += 1

    print(result)
    print(counter)
    print(total/len(result))
    pp = ''
    c = 0
    for i in counter:
        pp += '\t' + str(c)
        pp += '\t'
        pp += '(' + str(i) + ') :'
        pp += '\t'
        c += 1
        for j in range(0, i, 1000):
            pp += 'x'
        print(pp)
        pp = ''

class Person:
    def __init__(self, probability_table):
        self.p = probability_table
        self.age = 0
        self.dead = False
        self.male = random.choice([True, False])

    def inc_year(self):
        r = random.random()
        p = self.p[self.age]
        if r >= p:
            self.age += 1
            return True
        else:
            self.dead = True
            return False

    def is_dead(self):
        return self.dead

    def age(self):
        return self.age

    def sim_life(self):
        while not self.is_dead():
            self.inc_year()
        return self.age

life_sim(population_size=300000)


import time
import sys
import logging
from threading import Event, Thread, Lock
from queue import Queue, PriorityQueue, Empty
from urllib.parse import urlsplit
from traceback import format_exception
from collections import defaultdict
from copy import deepcopy
from datetime import datetime
from uuid import uuid4
from typing import cast, MutableMapping, Union

from bson import json_util as json

from .util import Pause

# from .loop import MultiCurlLoop
from .network_service import NetworkService
from .stat import Stat
from .request import Request, CallbackRequest, BaseRequest
from .error import get_error_tag, collect_error_context, DataNotValid, IowebConfigError
from .error_logger import ErrorLogger
from .proxylist import ProxyList

class Crawler(object):
    _taskgen_sleep_time = 0.01
    dataop_threshold_default = {
        "number": 500,
        "size": None,
        #'time': None,
    }
    dataop_threshold: MutableMapping[
        str, MutableMapping[str, Union[int, float, None]]
    ] = {}
    network_service_class = NetworkService

    def task_generator(self):
        if False:  # pylint: disable=using-constant-test
            yield None

    def __init__(
        self,
        network_threads=3,
        result_workers=4,
        num_task_generators=1,
        retry_limit=3,
        extra_data=None,
        stop_on_handler_error=False,
        debug=False,
        stat_logging=True,
        stat_logging_format="text",
        master_taskq=None,
    ):
        self.uuid = uuid4().hex
        logging.debug("Session ID: %s", self.uuid)
        if extra_data is None:
            self.extra_data = {}
        else:
            self.extra_data = deepcopy(extra_data)
        self.taskq = PriorityQueue()
        self.taskq_size_limit = max(100, network_threads * 2)
        self.resultq = Queue()
        self.shutdown_event = Event()
        self.fatal_error_happened = Event()
        self.retry_limit = retry_limit
        self.stat = Stat(
            speed_keys="crawler:request-processed",
            logging_enabled=stat_logging,
            logging_format=stat_logging_format,
        )
        self.fatalq = Queue()
        self.network = self.network_service_class(
            self.taskq,
            self.resultq,
            fatalq=self.fatalq,
            threads=network_threads,
            shutdown_event=self.shutdown_event,
            setup_request_hook=self.setup_request_hook,
            setup_request_proxy_hook=self.setup_request_proxy_hook,
            prepare_response_hook=self.prepare_response_hook,
            stat=self.stat,
        )
        self.result_workers = result_workers
        self.run_started = None

        self.dataopq = {}
        self.dataop_lock = defaultdict(Lock)
        self.dataop_counters = defaultdict(
            lambda: {
                "number": 0,
                "size": 0,
            }
        )
        self.error_logger = ErrorLogger()
        self.stop_on_handler_error = stop_on_handler_error
        self.proxylist = None
        self.debug = debug
        self.num_task_generators = num_task_generators
        self.master_taskq_cfg = master_taskq

        self.init_hook()

    @classmethod
    def update_arg_parser(cls, parser):
        for key, config in cls.extra_cli_args().items():
            parser.add_argument("--extra-%s" % key, **config)

    @classmethod
    def extra_cli_args(cls):
        return {}

    def load_proxylist(self, list_type, list_location, **kwargs):
        self.proxylist = ProxyList.create_from_source(
            list_type, list_location, **kwargs
        )

    def is_dataopq_dump_time(self, name):
        to_dump = False
        try:
            number_th = self.dataop_threshold[name]["number"]
        except KeyError:
            number_th = self.dataop_threshold_default["number"]
        if number_th:
            if self.dataop_counters[name]["number"] >= number_th:
                to_dump = True
        if not to_dump:
            try:
                size_th = self.dataop_threshold[name]["size"]
            except KeyError:
                size_th = self.dataop_threshold_default["size"]
            if size_th:
                if self.dataop_counters[name]["size"] >= size_th:
                    to_dump = True
        return to_dump

    def enq_dataop(self, name, op, size=None, force_dump=False):
        if op:
            self.enq_dataop_hook(name, op)
        self.dataop_lock[name].acquire()
        released = False
        try:
            self.dataopq.setdefault(name, [])
            if op:
                self.dataopq[name].append(op)
                self.dataop_counters[name]["number"] += 1
                if size:
                    self.dataop_counters[name]["size"] += size

            if force_dump or self.is_dataopq_dump_time(name):
                # logging.debug('Dumping data ops')
                self.stat.inc("dataop-dump-%s" % name)
                ops = self.dataopq[name]
                if ops:
                    self.dataopq[name] = []
                    self.dataop_counters[name]["number"] = 0
                    self.dataop_counters[name]["size"] = 0
                    # release as soon as possible
                    self.dataop_lock[name].release()
                    released = True
                    func = getattr(self, "dataop_handler_%s" % name)
                    func(ops)
                else:
                    self.dataop_lock[name].release()
                    released = True
        finally:
            if not released:
                self.dataop_lock[name].release()

    def enq_dataop_hook(self, name, op):
        pass

    def init_hook(self):
        pass

    def prepare_response_hook(self, transport, req, res, urllib3_response):
        pass

    def setup_request_proxy_hook(self, unused_transport, req):
        if self.proxylist:
            proxy = self.proxylist.random_server()
            req.setup(
                proxy=proxy.address(),
                proxy_auth=proxy.auth(),
                proxy_type=proxy.proxy_type,
            )
            # If requst is configured to not close connection (default)
            # and proxylist requires to close connection
            # then update request config
            if not req["close_connection"] and self.proxylist.close_connection:
                req.setup(close_connection=True)

    def setup_request_hook(self, transport, req):
        pass

    def submit_task(self, req):
        self.submit_task_hook(req)
        self.taskq.put((req.priority, req))

    def submit_task_hook(self, req):
        pass

    def task_generator_master(self):
        cfg = self.master_taskq_cfg
        if not cfg:
            raise IowebConfigError("Master task queue is not configured")
        backend_id = cfg.pop("backend", None)
        if backend_id not in ("redis",):
            raise IowebConfigError("Invalid master task queue backend: %s" % backend_id)
        # Redis specific things
        # TODO: move into backend class
        taskq_id = cfg.pop("taskq_id", None)
        if not taskq_id:
            raise IowebConfigError("Invalid queue ID: %s" % taskq_id)
        from redis import Redis  # pylint: disable=import-outside-toplevel

        rdb = Redis(decode_responses=True, **cfg)
        none_task_time = time.time()
        while True:
            task = rdb.lpop(taskq_id)
            if task is None:
                if time.time() - none_task_time > 5:
                    none_task_time = time.time()
                    self._flush_dataops()
                time.sleep(0.01)
            else:
                task = json.loads(task)
                if "$op" in task:
                    if task["$op"] == "shutdown":
                        return
                    else:
                        raise IowebConfigError(
                            "Got invalid $op from master taskq queue: %s" % task["$op"]
                        )
                else:
                    req = Request.from_data(task)
                    yield req

    def thread_task_generator(self):
        try:
            try:
                if self.master_taskq_cfg:
                    tgen_iter = self.task_generator_master()
                else:
                    tgen_iter = iter(self.task_generator())
            except TypeError:
                return
            else:
                for item in tgen_iter:
                    while item:
                        if self.shutdown_event.is_set():
                            return
                        if self.taskq.qsize() >= self.taskq_size_limit:
                            time.sleep(self._taskgen_sleep_time)
                        else:
                            self.submit_task(item)
                            item = None
        except (Exception, KeyboardInterrupt):  # pylint: disable=broad-except
            self.fatalq.put((sys.exc_info(), None))

    def is_result_ok(self, req, res):
        if res.error:
            return False
        elif isinstance(req, CallbackRequest):
            return True
        elif (
            0 < res.status < 400
            or res.status == 404
            or (
                req.config["extra_valid_status"]
                and res.status in req.config["extra_valid_status"]
            )
        ):
            return True
        else:
            return False

    def thread_result_processor(self, pause):
        error_ctx = None
        try:
            while not self.shutdown_event.is_set():
                if pause.pause_event.is_set():
                    pause.process_pause()
                try:
                    result = self.resultq.get(True, 0.1)
                except Empty:
                    pass
                else:
                    self.stat.inc("crawler:request-processed")
                    try:
                        if result["request"]["raw"] or self.is_result_ok(
                            result["request"],
                            result["response"],
                        ):
                            self.process_ok_result(result)
                        else:
                            self.process_fail_result(result)
                    except Exception:
                        error_ctx = collect_error_context(result["request"])
                        raise
        except (KeyboardInterrupt, Exception):  # pylint: disable=broad-except
            self.fatalq.put((sys.exc_info(), error_ctx))

    def thread_fatalq_processor(self):
        try:
            while not self.shutdown_event.is_set():
                try:
                    exc_info, ctx = self.fatalq.get(True, 0.1)
                except Empty:
                    pass
                else:
                    self.shutdown_event.set()
                    self.fatal_error_happened.set()
                    logging.error("Fatal exception")
                    if ctx:
                        logging.error("Exception context:")
                        for key, val in sorted(ctx.items(), key=lambda x: x[0]):
                            logging.error(" * %s: %s", key, val)
                    else:
                        logging.error("Exception context: N/A")
                    logging.error("".join(format_exception(*exc_info)))
                    if not isinstance(exc_info[1], KeyboardInterrupt):
                        self.log_error(exc_info, ctx)
        except Exception:
            self.shutdown_event.set()
            raise

    def log_error(self, exc_info, ctx=None):
        ctx = ctx or {}
        ctx.update(
            {
                "crawler_id": self.__class__.__name__,
                "date": datetime.utcnow().isoformat(),
            }
        )
        self.error_logger.log_error(exc_info, ctx)

    def thread_manager(self, task_gens, pauses):
        try:
            for th in task_gens:
                th.join()

            def system_is_busy():
                return (
                    self.taskq.qsize()
                    or self.resultq.qsize()
                    or len(self.network.active_handlers)
                )

            while not self.shutdown_event.is_set():
                while system_is_busy():
                    if self.shutdown_event.is_set():
                        return
                    time.sleep(0.1)

                for pause in pauses:
                    pause.pause_event.set()
                ok = True
                for pause in pauses:
                    if not pause.is_paused.wait(0.1):
                        ok = False
                        break
                if not ok:
                    for pause in pauses:
                        pause.pause_event.clear()
                        pause.resume_event.set()
                else:
                    if not system_is_busy():
                        for pause in pauses:
                            pause.pause_event.clear()
                            pause.resume_event.set()
                        self.shutdown_event.set()
                        return
                    else:
                        for pause in pauses:
                            pause.pause_event.clear()
                            pause.resume_event.set()
        except (KeyboardInterrupt, Exception):  # pylint: disable=broad-except
            self.fatalq.put((sys.exc_info(), None))

    def process_ok_result(self, result):
        self.stat.inc("crawler:request-ok")
        name = result["request"].config["name"]
        handler = getattr(self, "handler_%s" % name)
        try:
            handler_result = handler(
                result["request"],
                result["response"],
            )
            try:
                iter(handler_result)
            except TypeError:
                pass
            else:
                for item in handler_result:
                    if isinstance(item, BaseRequest):
                        self.submit_task(item)
                    else:
                        raise Exception("Handler yielded non request task: %s" % item)
        except Exception as ex:  # pylint: disable=broad-except
            self.stat.inc("result-handler-error:%s" % get_error_tag(ex))
            if isinstance(ex, DataNotValid):
                self.process_fail_result(result)
            else:
                if self.stop_on_handler_error:
                    raise
                else:
                    logging.exception("Error in result handler")
                    ctx = collect_error_context(result["request"])
                    self.log_error(sys.exc_info(), ctx)

    def process_fail_result(self, result):
        self.stat.inc("crawler:request-fail")
        req = result["request"]

        if result["response"].error:
            self.stat.inc("network-error:%s" % get_error_tag(result["response"].error))
        if result["response"].status:
            self.stat.inc("http:status-%s" % result["response"].status)

        if req.retry_count + 1 < self.retry_limit:
            self.stat.inc("crawler:request-retry")
            req.retry_count += 1
            req.priority = req.priority - 1
            self.submit_task(req)
        else:
            self.stat.inc("crawler:request-rejected")
            name = result["request"].config["name"]
            handler = getattr(self, "rejected_handler_%s" % name, None)
            if handler is None:
                handler = self.default_rejected_handler
            try:
                handler(result["request"], result["response"])
            except Exception as ex:  # pylint: disable=broad-except
                self.stat.inc("rejected-handler-error:%s" % get_error_tag(ex))
                if self.stop_on_handler_error:
                    raise
                else:
                    logging.exception("Error in rejected result handler")
                    ctx = collect_error_context(req)
                    self.log_error(sys.exc_info(), ctx)

    def default_rejected_handler(self, req, res):
        pass

    def run_hook(self):
        pass

    def thread_debug(self):
        try:
            while not self.shutdown_event.is_set():
                stat = []
                now = time.time()
                for hdl in self.network.active_handlers:
                    stat.append((hdl, self.network.registry[hdl]["start"]))
                with open("var/crawler.debug", "w", encoding="utf-8") as out:
                    for hdl, start in list(
                        sorted(stat, key=lambda x: (x[1] or now), reverse=True)
                    ):
                        req = cast(Request, self.network.registry[hdl]["request"])
                        out.write(
                            "%.2f - [#%s] - %s\n"
                            % (
                                (now - start) if start else 0,
                                req.retry_count if req else "NA",
                                urlsplit(req["url"]).netloc if req else "NA",
                            )
                        )
                    out.write(
                        "Active handlers: %d\n" % len(self.network.active_handlers)
                    )
                    out.write("Idle handlers: %d\n" % len(self.network.idle_handlers))
                    out.write("Taskq size: %d\n" % self.taskq.qsize())
                    out.write("Resultq size: %d\n" % self.resultq.qsize())

                total = 0
                count = 0
                for hdl, start in stat:
                    if start:
                        total += now - start
                        count += 1
                logging.debug(
                    "Median handler time: %.2f", (total / count) if count else 0
                )

                self.shutdown_event.wait(3)
        except (KeyboardInterrupt, Exception):  # pylint: disable=broad-except
            self.fatalq.put((sys.exc_info(), None))

    def thread_network(self):
        try:
            self.network.run()
        except (KeyboardInterrupt, Exception):  # pylint: disable=broad-except
            self.fatalq.put((sys.exc_info(), None))

    def _flush_dataops(self):
        for name in list(self.dataopq.keys()):
            self.enq_dataop(name, None, force_dump=True)

    def shutdown(self):
        self._flush_dataops()
        self.shutdown_hook()
        if self.stat.th_export:
            self.stat.th_export_dump_stat()

    def shutdown_hook(self):
        pass

    def run(self):
        try:
            self.run_hook()
            self.run_started = time.time()

            th_fatalq_proc = Thread(target=self.thread_fatalq_processor)
            th_fatalq_proc.start()

            task_gens = []
            if self.master_taskq_cfg and self.num_task_generators > 1:
                raise IowebConfigError(
                    "num_task_generators must be 1 in" " master task queue mode"
                )
            for _ in range(self.num_task_generators):
                th = Thread(target=self.thread_task_generator)
                th.start()
                task_gens.append(th)

            th_debug = Thread(target=self.thread_debug)
            if self.debug:
                th_debug.start()

            pauses = [x["pause"] for x in self.network.registry.values()]
            result_workers = []
            for _ in range(self.result_workers):
                pause = Pause()
                th = Thread(
                    target=self.thread_result_processor,
                    args=[pause],
                )
                pauses.append(pause)
                th.start()
                result_workers.append(th)

            th_manager = Thread(
                target=self.thread_manager,
                args=[task_gens, pauses],
            )
            th_manager.start()

            th_network = Thread(
                target=self.thread_network,
            )
            th_network.start()

            th_manager.join()
            th_fatalq_proc.join()
            for th in task_gens:
                th.join()
            if self.debug:
                th_debug.join()
            for th in result_workers:
                th.join()

        except (Exception, KeyboardInterrupt):
            self.fatal_error_happened.set()
            raise
        finally:
            self.shutdown_event.set()
            self.shutdown()