package de.renew.gui.nin;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

import de.renew.draw.storables.ontology.Drawing;
import de.renew.gui.ArcConnection;
import de.renew.gui.CPNTextFigure;
import de.renew.gui.TransitionFigure;

/**
 * A PTCTransitionBuilder can generate a new {@link TransitionFigure}
 * and add it to a given {@link Drawing}.
 * It generates the transition under a certain condition:
 * A transition only gets build if two given transition, one with
 * a downlink and one with an uplink, can synchronize with each other.
 * Now, a transition gets built which has the same effect as that
 * synchronization, in regard to the respective pre- and post-sets
 * of the up- and downlinks.
 */
public class PTCTransitionBuilder {

    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(PTCTransitionBuilder.class);
    private final Drawing drawingToModify;
    private final PTCNetInformation netInformation;

    /**
     * Creates a new <code>PTCTransitionBuilder</code> instance.
     *
     * @param drawingToAddTransitionsTo add transitions to this drawing.
     * @param netInformation            information holder for the net.
     **/
    public PTCTransitionBuilder(
        Drawing drawingToAddTransitionsTo, PTCNetInformation netInformation)
    {
        this.drawingToModify = drawingToAddTransitionsTo;
        this.netInformation = netInformation;
    }

    /**
     * This method is the main entry point for the generation and
     * adding of {@link TransitionFigure}s.
     * It checks if a down- and an uplink could synchronize and
     * builds a transition for that synchronization.
     *
     * @param uplinkTransition  figure of the uplink
     * @param partnerTransition figure of the communication partner
     */
    public void buildTransitionIfBindingIsPossible(
        TransitionFigure uplinkTransition, TransitionFigure partnerTransition)
    {
        CPNTextFigure uplinkText =
            netInformation.getInitialTransitionInscriptions().get(uplinkTransition);
        Optional<List<String>> params = getChannelTransitionTextValues(uplinkText);
        CPNTextFigure partnerText =
            netInformation.getInitialTransitionInscriptions().get(partnerTransition);
        Optional<List<String>> partnerParams = getChannelTransitionTextValues(partnerText);

        // If there are no parameters
        if (params.isPresent() && partnerParams.isPresent()) {
            if (params.get().isEmpty() && partnerParams.get().isEmpty()) {
                buildTransition(
                    uplinkTransition, partnerTransition, new HashMap<>(), drawingToModify);
            } else {
                Map<String, Integer> bindings = bindingMap(partnerParams.get(), params.get());
                if (bindings != null) {
                    buildTransition(uplinkTransition, partnerTransition, bindings, drawingToModify);
                }
            }
        } else {
            logger.error("Could not read channel parameters.");
        }
    }

    /**
     * This method takes two transitions which can synchronize and builds
     * one transition that can execute their specific synchronization.
     * The transition's pre-/post-set is taken from the combined pre-/post-
     * set of both communicating transitions.
     *
     * @param uplinkFigure      the uplink transition
     * @param downlinkFigure    the downlink transition
     * @param parameterBindings map of all variable inscriptions to their bound integers
     * @param netdrawing        drawing to which the new transition gets added
     */
    private void buildTransition(
        TransitionFigure uplinkFigure, TransitionFigure downlinkFigure,
        Map<String, Integer> parameterBindings, Drawing netdrawing)
    {
        TransitionFigure t = (TransitionFigure) downlinkFigure.clone();

        addArcsForIncomingArcs(downlinkFigure, t, netdrawing, parameterBindings);
        addArcsForIncomingArcs(uplinkFigure, t, netdrawing, parameterBindings);

        // Add arc for downlinks's post-set arcs
        addArcsForOutgoingArcs(downlinkFigure, t, netdrawing, parameterBindings);
        // Add arc for uplinks's post-set arcs
        addArcsForOutgoingArcs(uplinkFigure, t, netdrawing, parameterBindings);

        netdrawing.add(t);
        netdrawing.checkDamage();
    }

    private void addArcsForIncomingArcs(
        TransitionFigure oldTransition, TransitionFigure newTransition, Drawing netdrawing,
        Map<String, Integer> parameterBindings)
    {
        for (ArcConnection arc : netInformation.getIncomingArcs(oldTransition)) {
            String[] arcInscriptions =
                getArcInscriptionVariables(netInformation.getInitialArcInscriptions().get(arc));
            // If there are no parameters or the arc is not inscripted..
            if (parameterBindings.isEmpty() || arcInscriptions == null) {
                addIncomingArc(arc, newTransition, netdrawing);
            } else {
                for (String variable : parameterBindings.keySet()) {
                    // TODO: Might cause problems if two arc inscriptions have the same name (x and x)
                    for (String arcInscription : arcInscriptions) {
                        if (variable.equals(arcInscription)) {
                            addIncomingArcWithInscription(
                                arc, parameterBindings.get(variable), newTransition, netdrawing);
                        }
                    }
                }
            }
        }
    }

    //TODO: Refactor into better methods. Currently, there is a lot of code duplication
    private void addIncomingArcWithInscription(
        ArcConnection arc, Integer parameterValue, TransitionFigure newTransition,
        Drawing netdrawing)
    {
        if (parameterValue == 0) {
            return;
        }
        ArcConnection addedArc = (ArcConnection) arc.clone();
        addedArc.connectStart(arc.start());
        addedArc.connectEnd(newTransition.connectorAt(newTransition.center()));
        netdrawing.add(addedArc);
        if (parameterValue != 1) {
            CPNTextFigure inscription = new CPNTextFigure(CPNTextFigure.INSCRIPTION);
            inscription.setText(Integer.toString(parameterValue));
            addInscription(inscription, addedArc, netdrawing);
        }
    }

    /**
     * Look at each of the outgoing arcs.
     * Add an according arc to the new transition.
     *
     * @param oldTransition     add according arcs of this transition to the new transition
     * @param newTransition     add arcs to this transition
     * @param netdrawing        add arcs to this drawing
     * @param parameterBindings mapping from the parameters of the channel to its integer bindings
     */
    private void addArcsForOutgoingArcs(
        TransitionFigure oldTransition, TransitionFigure newTransition, Drawing netdrawing,
        Map<String, Integer> parameterBindings)
    {
        for (ArcConnection arc : netInformation.getOutgoingArcs(oldTransition)) {
            String[] arcInscriptions =
                getArcInscriptionVariables(netInformation.getInitialArcInscriptions().get(arc));
            // If there are no parameters or the arc is not inscripted..
            if (parameterBindings.isEmpty() || arcInscriptions == null) {
                addOutgoingArc(arc, newTransition, netdrawing);
            } else {
                for (String variable : parameterBindings.keySet()) {
                    for (String arcInscription : arcInscriptions) {
                        if (variable.equals(arcInscription)) {
                            addOutgoingArcWithInscription(
                                arc, parameterBindings.get(variable), newTransition, netdrawing);
                        }
                    }
                }
            }
        }
    }

    /**
     * Add an arc to a figure as an outgoing arc.
     * Use its original inscription.
     *
     * @param arc        arc to connect to the transition
     * @param transition figure of the connected transition
     * @param drawing    drawing in which the connection takes place
     */
    private void addOutgoingArc(ArcConnection arc, TransitionFigure transition, Drawing drawing) {
        ArcConnection addedArc = (ArcConnection) arc.clone();
        addedArc.connectEnd(arc.end());
        addedArc.connectStart(transition.connectorAt(transition.center()));
        drawing.add(addedArc);
        CPNTextFigure text = netInformation.getInitialArcInscriptions().get(arc);
        if (text != null) {
            addInscription(text, addedArc, drawing);
        }
    }

    /**
     * Add an arc to a figure as an outgoing arc.
     * The inscription to use is given as an integer.
     * If the inscription is 0, no arc gets built.
     * If the inscription value is 1, no inscription get added.
     *
     * @param arc            original arc
     * @param parameterValue value to inscribe to the arc
     * @param uplinkFigure   figure of the connected transition
     * @param drawing        drawing to add arc to
     */
    private void addOutgoingArcWithInscription(
        ArcConnection arc, int parameterValue, TransitionFigure uplinkFigure, Drawing drawing)
    {
        if (parameterValue == 0) {
            return;
        }
        ArcConnection addedArc = (ArcConnection) arc.clone();
        addedArc.connectEnd(arc.end());
        addedArc.connectStart(uplinkFigure.connectorAt(uplinkFigure.center()));
        drawing.add(addedArc);
        if (parameterValue != 1) {
            CPNTextFigure inscription = new CPNTextFigure(CPNTextFigure.INSCRIPTION);
            inscription.setText(Integer.toString(parameterValue));
            addInscription(inscription, addedArc, drawing);
        }
    }

    /**
     * Add an arc to a figure as an incoming arc.
     * Use its original inscription.
     *
     * @param arc        Arc to connect to the transition
     * @param transition figure of the connected transition
     * @param drawing    drawing in which the connection takes place
     */
    private void addIncomingArc(ArcConnection arc, TransitionFigure transition, Drawing drawing) {
        ArcConnection addedArc = (ArcConnection) arc.clone();
        addedArc.connectStart(arc.start());
        addedArc.connectEnd(transition.connectorAt(transition.center()));
        drawing.add(addedArc);
        CPNTextFigure text = netInformation.getInitialArcInscriptions().get(arc);
        if (text != null) {
            addInscription(text, addedArc, drawing);
        }
    }

    /**
     * Add an inscription given as a {@link CPNTextFigure} to an
     * arc and a drawing.
     *
     * @param text    text to inscribe
     * @param newArc  arc to add the inscription to
     * @param drawing drawing to add the inscription to
     */
    private void addInscription(CPNTextFigure text, ArcConnection newArc, Drawing drawing) {
        CPNTextFigure inscription = (CPNTextFigure) text.clone();
        drawing.add(inscription);
        newArc.addChild(inscription);
        inscription.setParent(newArc);
    }

    /**
     * Check if two transition could bind based on the parameters
     * of their inscriptions.
     * If they can, return the parameter mapping.
     * If they can't, return null.
     *
     * @param downlinkParameters all parameters of the downlink
     * @param uplinkParameters   all parameters of the uplink
     * @return Mapping of variable parameters to their integer bindings
     */
    private Map<String, Integer> bindingMap(
        List<String> downlinkParameters, List<String> uplinkParameters)
    {
        // Can only bind if digits ("Stelligkeit") match.
        if (downlinkParameters.size() != uplinkParameters.size()) {
            return null;
        }
        Map<String, Integer> numbersOfVariableParameters = new HashMap<>();
        Pattern pattern = Pattern.compile("\\d+");
        for (int i = 0; i < downlinkParameters.size(); i++) {
            if (pattern.matcher(downlinkParameters.get(i)).matches()) {
                if (pattern.matcher(uplinkParameters.get(i)).matches()) {
                    return null;
                } else {
                    numbersOfVariableParameters
                        .put(uplinkParameters.get(i), Integer.parseInt(downlinkParameters.get(i)));
                }
            } else {
                if (pattern.matcher(uplinkParameters.get(i)).matches()) {
                    numbersOfVariableParameters
                        .put(downlinkParameters.get(i), Integer.parseInt(uplinkParameters.get(i)));
                } else {
                    return null;
                }
            }
        }
        return numbersOfVariableParameters;
    }

    /**
     * Parse through an arc inscription in the form of a
     * {@link CPNTextFigure} and fill an
     * array which holds the names of all its parameters.
     * <p>
     * Example: If an arc is inscribed with "x;y",
     * this method returns the following String array: [x, y].
     *
     * @param text the uplink inscription to parse
     * @return an array with all its parameters
     */
    private String[] getArcInscriptionVariables(CPNTextFigure text) {
        try {
            List<String> variables = new ArrayList<>();
            for (String line : text.getLines()) {
                String[] variableLine = line.trim().split(";");
                variables.addAll(List.of(variableLine));
            }
            return variables.toArray(String[]::new);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Parse through an inscription in the form of a
     * {@link CPNTextFigure} and fill a
     * list which holds the values of all its parameters.
     * <p>
     * Example: If a downlink is inscribed with "this:(1, 2)",
     * this method returns the following list: {1, 2}.
     *
     * @param text the inscription to parse
     * @return an array with all its parameters
     */
    private Optional<List<String>> getChannelTransitionTextValues(CPNTextFigure text) {
        try {
            List<String> variables = new ArrayList<>();
            for (String line : text.getLines()) {
                line = line.substring(line.indexOf("(") + 1, line.indexOf(")"));
                if (line.isBlank()) {
                    return Optional.of(variables);
                }
                String[] variableInLine = line.split(",");
                Arrays.parallelSetAll(variableInLine, (i) -> variableInLine[i].trim());
                variables.addAll(List.of(variableInLine));
            }
            return Optional.of(variables);
        } catch (Exception e) {
            return Optional.empty();
        }
    }
}
