package application.editor.stages;

import algorithms.Validation;
import application.editor.Editor;
import application.editor.FlowCellEditor;
import application.editor.Stage;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGeometry;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxGraphModel.mxTerminalChange;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.util.mxPoint;
import models.algebra.Expression;
import models.dataConstraintModel.Channel;
import models.dataConstraintModel.ChannelMember;
import models.dataConstraintModel.ResourcePath;
import models.dataFlowModel.DataTransferChannel;
import models.dataFlowModel.DataTransferModel;
import models.visualModel.FormulaChannel;
import parser.Parser;
import parser.exceptions.*;

import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;

public class DataFlowModelingStage extends Stage {

    public DataFlowModelingStage(mxGraphComponent graphComponent) {
        super(graphComponent);
    }

    @Override
    public void init(Stage prevStage) {
    }

    @Override
    public FlowCellEditor createCellEditor(mxGraphComponent graphComponent) {
        return new DataFlowCellEditor(this, graphComponent);
    }

    @Override
    public mxIEventListener createChangeEventListener(Editor editor) {
        return (sender, event) -> {
            List<mxCell> terminals = new ArrayList<>();
            mxCell cell = null;
            for (Object change : ((List) event.getProperties().get("changes"))) {
                if (change instanceof mxTerminalChange) {
                    mxTerminalChange terminalChange = (mxTerminalChange) change;
                    cell = (mxCell) terminalChange.getCell();
                    mxCell terminal = (mxCell) terminalChange.getTerminal();
                    terminals.add(terminal);
                }
            }
            if (terminals.size() == 2) {
                if (!editor.connectEdge(cell, terminals.get(0), terminals.get(1))) {
                    graph.removeCells(new mxCell[]{cell});
                    graph.clearSelection();
                }
            }
        };
    }

    @Override
    public MouseListener createMouseEventListener(Editor editor) {
        return null;
    }

    @Override
    public boolean canChangeFrom(Stage prevStage) {
        return true;
    }

    public void clear() {
        model = null;

        ((mxGraphModel) graph.getModel()).clear();
    }

    public DataTransferModel getModel() {
        if (model == null) {
            model = new DataTransferModel();
        }
        return model;
    }

    public void setModel(DataTransferModel model) {
        // Clear the mxGraph
        clear();

        this.model = model;
    }

    public boolean isValid() {
        if (model == null) {
            return false;
        }
        return Validation.checkUpdateConflict(model);
    }

    public void addResourcePath(ResourcePath parentPath, String resName) {
        ResourcePath resourcePath;
        if (parentPath == null) {
            resourcePath = new ResourcePath(resName);
            getModel().addResourcePath(resourcePath);
        } else {
            if (resName.startsWith(Parser.LEFT_CURLY_BRACKET) && resName.endsWith(Parser.RIGHT_CURLY_BRACKET)) {
                Parser.TokenStream stream = new Parser.TokenStream();
                Parser parser = new Parser(stream);
                stream.addLine(resName.substring(1, resName.length() - 1));
                try {
                    Expression exp = parser.parseTerm(stream, getModel());
                    resourcePath = new ResourcePath(parentPath, exp);
                    getModel().addResourcePath(resourcePath);
                } catch (ExpectedRightBracket | WrongJsonExpression | ExpectedColon | ExpectedDoubleQuotation e) {
                    e.printStackTrace();
                }
            } else {
                resourcePath = new ResourcePath(parentPath, resName);
                getModel().addResourcePath(resourcePath);
            }
        }

        graph.getModel().beginUpdate();
        mxCell root = (mxCell) graph.getDefaultParent();
        try {
            graph.insertVertex(root, null, resName, 20, 20, 80, 30, "shape=ellipse;perimeter=ellipsePerimeter"); // insert a resource as a vertex
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void addChannel(DataTransferChannel channelGen) {
        getModel().addChannel(channelGen);

        graph.getModel().beginUpdate();
        mxCell root = (mxCell) graph.getDefaultParent();
        try {
            mxGeometry geo1 = new mxGeometry(0, 0.5, PORT_DIAMETER, PORT_DIAMETER);
            geo1.setOffset(new mxPoint(-PORT_RADIUS, -PORT_RADIUS));
            geo1.setRelative(true);

            mxGeometry geo2 = new mxGeometry(1.0, 0.5, PORT_DIAMETER, PORT_DIAMETER);
            geo2.setOffset(new mxPoint(-PORT_RADIUS, -PORT_RADIUS));
            geo2.setRelative(true);

            Object channel = graph.insertVertex(root, null, channelGen.getChannelName(), 150, 20, 30, 30); // insert a channel as a vertex
            mxCell port_in = new mxCell(null, geo1, "shape=ellipse;perimter=ellipsePerimeter");
            port_in.setVertex(true);
            graph.addCell(port_in, channel);        // insert the input port of a channel
            mxCell port_out = new mxCell(null, geo2, "shape=ellipse;perimter=ellipsePerimeter");
            port_out.setVertex(true);
            graph.addCell(port_out, channel);        // insert the output port of a channel
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void addIOChannel(DataTransferChannel ioChannelGen) {
        getModel().addInputChannel(ioChannelGen);

        graph.getModel().beginUpdate();
        mxCell root = (mxCell) graph.getDefaultParent();
        try {
            mxGeometry geo2 = new mxGeometry(1.0, 0.5, PORT_DIAMETER, PORT_DIAMETER);
            geo2.setOffset(new mxPoint(-PORT_RADIUS, -PORT_RADIUS));
            geo2.setRelative(true);

            Object channel = graph.insertVertex(root, null, ioChannelGen.getChannelName(), 150, 20, 30, 30); // insert an I/O channel as a vertex
            mxCell port_out = new mxCell(null, geo2, "shape=ellipse;perimter=ellipsePerimeter");
            port_out.setVertex(true);
            graph.addCell(port_out, channel);        // insert the output port of a channel
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void addFormulaChannel(FormulaChannel formulaChannelGen) {
        getModel().addChannel(formulaChannelGen);

        graph.getModel().beginUpdate();
        mxCell root = (mxCell) graph.getDefaultParent();
        try {
            mxGeometry geo1 = new mxGeometry(0, 0.5, PORT_DIAMETER, PORT_DIAMETER);
            geo1.setOffset(new mxPoint(-PORT_RADIUS, -PORT_RADIUS));
            geo1.setRelative(true);

            mxGeometry geo2 = new mxGeometry(1.0, 0.5, PORT_DIAMETER, PORT_DIAMETER);
            geo2.setOffset(new mxPoint(-PORT_RADIUS, -PORT_RADIUS));
            geo2.setRelative(true);

            Object channel = graph.insertVertex(root, null, formulaChannelGen.getChannelName(), 150, 20, 30, 30); // insert a channel as a vertex
            mxCell port_in = new mxCell(null, geo1, "shape=ellipse;perimter=ellipsePerimeter");
            port_in.setVertex(true);
            graph.addCell(port_in, channel);        // insert the input port of a channel
            mxCell port_out = new mxCell(null, geo2, "shape=ellipse;perimter=ellipsePerimeter");
            port_out.setVertex(true);
            graph.addCell(port_out, channel);        // insert the output port of a channel
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public boolean connectEdge(mxCell edge, mxCell src, mxCell dst) {
        DataTransferModel model = getModel();
        Channel srcCh = model.getChannel((String) src.getValue());
        if (srcCh == null) {
            srcCh = model.getInputChannel((String) src.getValue());
            if (srcCh == null) {
                ResourcePath srcRes = model.getResourcePath((String) src.getValue());
                Channel dstCh = model.getChannel((String) dst.getValue());
                if (srcRes == null || dstCh == null) {
                    return false;
                }
                // resource to channel edge
                ChannelMember srcCm = new ChannelMember(srcRes);
                ((DataTransferChannel) dstCh).addChannelMemberAsInput(srcCm);
                edge.setValue(new Editor.SrcDstAttribute(srcRes, dstCh));
                return true;
            }
        }
        ResourcePath dstRes = model.getResourcePath((String) dst.getValue());
        if (dstRes == null) {
            return false;
        }
        // channel to resource edge
        ChannelMember dstCm = new ChannelMember(dstRes);
        ((DataTransferChannel) srcCh).addChannelMemberAsOutput(dstCm);
        edge.setValue(new Editor.SrcDstAttribute(srcCh, dstRes));
        return true;
    }

    public void delete() {
        for (Object obj : graph.getSelectionCells()) {
            mxCell cell = (mxCell) obj;
            if (cell.isEdge()) {
                String srcName = (String) cell.getSource().getValue();
                String dstName = (String) cell.getTarget().getValue();
                if (model.getResourcePath(srcName) != null) {
                    // resource to channel edge
                    Channel ch = model.getChannel(dstName);
                    ch.removeChannelMember(model.getResourcePath(srcName));
                } else if (model.getResourcePath(dstName) != null) {
                    // channel to resource edge
                    Channel ch = model.getChannel(srcName);
                    if (ch == null) {
                        ch = model.getInputChannel(srcName);
                    }
                    ch.removeChannelMember(model.getResourcePath(dstName));
                }
            } else if (cell.isVertex()) {
                String name = (String) cell.getValue();
                if (model.getChannel(name) != null) {
                    model.removeChannel(name);
                } else if (model.getInputChannel(name) != null) {
                    model.removeInputChannel(name);
                } else if (model.getResourcePath(name) != null) {
                    model.removeResourcePath(name);
                }
            }
        }
        graph.removeCells(graph.getSelectionCells());
    }

    public void setChannelCode(DataTransferChannel ch, String code) {
        ch.setSourceText(code);
        Parser.TokenStream stream = new Parser.TokenStream();
        Parser parser = new Parser(stream);

        for (String line : code.split("\n")) {
            stream.addLine(line);
        }
        try {
            DataTransferChannel ch2 = parser.parseChannel(getModel());
            for (ChannelMember chm2 : ch2.getInputChannelMembers()) {
                for (ChannelMember chm : ch.getInputChannelMembers()) {
                    if (chm2.getResource() == chm.getResource()) {
                        chm.setStateTransition(chm2.getStateTransition());
                        break;
                    }
                }
            }
            for (ChannelMember chm2 : ch2.getOutputChannelMembers()) {
                for (ChannelMember chm : ch.getOutputChannelMembers()) {
                    if (chm2.getResource() == chm.getResource()) {
                        chm.setStateTransition(chm2.getStateTransition());
                        break;
                    }
                }
            }
            for (ChannelMember chm2 : ch2.getReferenceChannelMembers()) {
                for (ChannelMember chm : ch.getReferenceChannelMembers()) {
                    if (chm2.getResource() == chm.getResource()) {
                        chm.setStateTransition(chm2.getStateTransition());
                        break;
                    }
                }
            }
        } catch (ExpectedRightBracket | ExpectedChannel | ExpectedChannelName | ExpectedLeftCurlyBracket |
                 ExpectedInOrOutOrRefOrSubKeyword | ExpectedStateTransition | ExpectedEquals | ExpectedRHSExpression |
                 WrongLHSExpression | WrongRHSExpression | ExpectedAssignment | ExpectedRightCurlyBracket |
                 WrongPathExpression | WrongJsonExpression | ExpectedColon | ExpectedDoubleQuotation e) {
            e.printStackTrace();
        }
    }
}
