package application.editor;

import application.editor.stages.DataFlowModelingStage;
import application.editor.stages.PushPullSelectionStage;
import application.layouts.DAGLayout;
import code.ast.CompilationUnit;
import com.mxgraph.layout.mxCircleLayout;
import com.mxgraph.layout.mxCompactTreeLayout;
import com.mxgraph.model.mxCell;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.mxEvent;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxCellState;
import com.mxgraph.view.mxGraph;
import com.mxgraph.view.mxGraphView;
import models.EdgeAttribute;
import models.dataConstraintModel.Channel;
import models.dataConstraintModel.ResourcePath;
import models.dataFlowModel.DataFlowGraph;
import models.dataFlowModel.DataTransferChannel;
import models.dataFlowModel.DataTransferModel;
import models.visualModel.FormulaChannel;
import parser.Parser;
import parser.ParserDTRAM;
import parser.exceptions.*;

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

public class Editor {

    protected DataTransferModel model = null;

    protected mxGraph graph;
    private final mxGraphComponent graphComponent;

    protected Stage curStage;
    private final List<IStageChangeListener> stageChangeListeners;

    private mxIEventListener curChangeEventListener = null;
    private MouseListener curMouseEventListener = null;

    protected String curFileName = null;
    protected String curFilePath = null;
    protected ArrayList<CompilationUnit> codes = null;

    public static DataFlowModelingStage STAGE_DATA_FLOW_MODELING = null;
    public static PushPullSelectionStage STAGE_PUSH_PULL_SELECTION = null;

    public Editor(mxGraphComponent graphComponent) {
        this.graphComponent = graphComponent;
        this.graph = graphComponent.getGraph();

        STAGE_DATA_FLOW_MODELING = new DataFlowModelingStage(graphComponent);
        STAGE_PUSH_PULL_SELECTION = new PushPullSelectionStage(graphComponent);

        graphComponent.setCellEditor(STAGE_DATA_FLOW_MODELING.createCellEditor(graphComponent));

        curStage = STAGE_DATA_FLOW_MODELING;

        stageChangeListeners = new ArrayList<>();
    }

    public boolean canChange(Stage nextStage) {
        return nextStage.canChangeFrom(curStage);
    }

    public boolean changeStage(Stage nextStage) {
        if (!nextStage.canChangeFrom(curStage)) {
            return false;
        }
        nextStage.init(curStage);
        graphComponent.setCellEditor(nextStage.createCellEditor(graphComponent));

        // add listeners
        // "curChangeEventListener" will be called when updating the mxGraph.
        if (curChangeEventListener != null) {
            graph.getModel().removeListener(curChangeEventListener);
        }
        curChangeEventListener = nextStage.createChangeEventListener(this);
        if (curChangeEventListener != null) {
            graph.getModel().addListener(mxEvent.CHANGE, curChangeEventListener);
        }

        // A handler of a mouse event.
        if (curMouseEventListener != null) {
            graphComponent.getGraphControl().removeMouseListener(curMouseEventListener);
        }
        curMouseEventListener = nextStage.createMouseEventListener(this);
        if (curMouseEventListener != null) {
            graphComponent.getGraphControl().addMouseListener(curMouseEventListener);
        }
        curStage = nextStage;
        notifyStageChangeListeners();
        return true;
    }

    public void addStageChangeListener(IStageChangeListener stageChangeListener) {
        stageChangeListeners.add(stageChangeListener);
    }

    private void notifyStageChangeListeners() {
        for (IStageChangeListener l : stageChangeListeners) {
            l.stageChanged(curStage);
        }
    }

    public DataFlowGraph getDataFlowGraph() {
        if (curStage instanceof PushPullSelectionStage) {
            return ((PushPullSelectionStage) curStage).getDataFlowGraph();
        }
        return null;
    }

    public void setCurFilePath(String curFilePath) {
        this.curFilePath = curFilePath;
        this.curFileName = new File(curFilePath).getName();
    }

    public void clear() {
        // Force to change to the data-flow modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (!stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).clear();

        model = null;
        curFilePath = null;
        curFileName = null;
        codes = null;
    }

    /**
     * Open a given file, parse the file, construct a DataFlowModel and a mxGraph
     *
     * @param file given file
     */
    public void open(File file) {
        // Force to change to data-modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (!stageChanged) {
            return;
        }
        if (file == null || !file.exists()) {
            return;
        }
        // get a file's extension
        String extension = file.getName().substring(file.getName().lastIndexOf("."));
        if (extension.contains(".model")) {
            openModel(file);
        }
        if (extension.contains(".dtram")) {
            openDTRAM(file);
        }
    }

    private void openModel(File file) {
        try {
            Parser parser = new Parser(new BufferedReader(new FileReader(file)));
            try {
                // Parse the .model file.
                model = parser.doParse();

                // Update stage's model to new parsed one
                if (curStage instanceof DataFlowModelingStage) {
                    ((DataFlowModelingStage) curStage).setModel(model);
                }

                // Force to change PushPullSelectionStage to construct mxGraph
                boolean stageChanged = changeStage(STAGE_PUSH_PULL_SELECTION);
                if (stageChanged && curStage instanceof PushPullSelectionStage) {
                    ((PushPullSelectionStage) curStage).constructGraph();
                }

                // Set layout
                setDAGLayout();

                // Update current file info
                curFilePath = file.getAbsolutePath();
                curFileName = file.getName();
            } catch (ExpectedChannel | ExpectedChannelName | ExpectedLeftCurlyBracket |
                     ExpectedInOrOutOrRefOrSubKeyword | ExpectedStateTransition | ExpectedEquals |
                     ExpectedRHSExpression | WrongLHSExpression | WrongRHSExpression | ExpectedRightBracket |
                     ExpectedAssignment | ExpectedRightCurlyBracket | WrongPathExpression | WrongJsonExpression |
                     ExpectedColon | ExpectedDoubleQuotation e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    private void openDTRAM(File file) {
        try {
            ParserDTRAM parser = new ParserDTRAM(new BufferedReader(new FileReader(file)));
            try {
                // Parse the .dtram file.
                model = parser.doParseModel();

                // Update stage's model to new parsed one
                if (curStage instanceof DataFlowModelingStage) {
                    ((DataFlowModelingStage) curStage).setModel(model);
                }

                // Force to change PushPullSelectionStage to construct mxGraph
                boolean stageChanged = changeStage(STAGE_PUSH_PULL_SELECTION);
                if (stageChanged && curStage instanceof PushPullSelectionStage) {
                    ((PushPullSelectionStage) curStage).constructGraph();
                }

                // Restore the geometry
                parser.doParseGeometry(graph);

                // Update current file info
                curFilePath = file.getAbsolutePath();
                curFileName = file.getName();
            } catch (ExpectedChannel | ExpectedChannelName | ExpectedLeftCurlyBracket |
                     ExpectedInOrOutOrRefOrSubKeyword | ExpectedStateTransition | ExpectedEquals |
                     ExpectedRHSExpression | WrongLHSExpression | WrongRHSExpression | ExpectedRightBracket |
                     ExpectedAssignment | ExpectedModel | ExpectedGeometry | ExpectedNode | ExpectedResource |
                     ExpectedFormulaChannel | ExpectedIoChannel | ExpectedRightCurlyBracket | WrongPathExpression |
                     WrongJsonExpression | ExpectedColon | ExpectedDoubleQuotation e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void save() {
        if (curFilePath == null) {
            return;
        }
        File file = new File(curFilePath);
        if (!file.exists()) {
            return;
        }
        // get a file's extension
        String extension = file.getName().substring(file.getName().lastIndexOf("."));
        if (extension.contains(".model")) {
            saveModel(file);
        }
        if (extension.contains(".dtram")) {
            saveDTRAM(file);
        }
    }

    private void saveModel(File file) {
        try {
            FileWriter filewriter = new FileWriter(file);
            filewriter.write(model.getSourceText());
            filewriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void saveDTRAM(File file) {
        try {
            FileWriter filewriter = new FileWriter(file);
            filewriter.write(toOutputString());
            filewriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * get writing texts "dtram" file  information is written.
     *
     * @return formatted "dtram" info texts.
     */
    private String toOutputString() {
        StringBuilder fileString = new StringBuilder();

        fileString.append("model {\n");
        fileString.append(model.getSourceText());
        fileString.append("}\n");

        fileString.append("geometry {\n");

        Object root = graph.getDefaultParent();
        for (int i = 0; i < graph.getModel().getChildCount(root); i++) {
            Object cell = graph.getModel().getChildAt(root, i);
            if (graph.getModel().isVertex(cell)) {
                mxGraphView view = graph.getView();
                mxCellState state = view.getState(cell);
                int x = (int) state.getX();
                int y = (int) state.getY();
                int w = (int) state.getWidth();
                int h = (int) state.getHeight();

                for (Channel ch : model.getChannels()) {
                    if (ch instanceof FormulaChannel && state.getLabel().equals(ch.getChannelName())) {
                        fileString.append("\tnode fc ").append(state.getLabel()).append(":").append(x).append(",").append(y).append(",").append(w).append(",").append(h).append("\n");
                    } else if (ch != null && state.getLabel().equals(ch.getChannelName())) {
                        fileString.append("\tnode c ").append(state.getLabel()).append(":").append(x).append(",").append(y).append(",").append(w).append(",").append(h).append("\n");
                    }
                }

                for (ResourcePath res : model.getResourcePaths()) {
                    if (res != null && state.getLabel().equals(res.getLeafResourceName()))
                        fileString.append("\tnode r ").append(state.getLabel()).append(":").append(x).append(",").append(y).append(",").append(w).append(",").append(h).append("\n");
                }

                for (Channel ioC : model.getInputChannels()) {
                    if (ioC != null && state.getLabel().equals(ioC.getChannelName())) {
                        fileString.append("\tnode ioc ").append(state.getLabel()).append(":").append(x).append(",").append(y).append(",").append(w).append(",").append(h).append("\n");
                    }
                }
            }
        }
        fileString.append("}\n");
        return fileString.toString();
    }

    public void delete() {
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (!stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).delete();
    }

    public void setDAGLayout() {
        mxCell root = (mxCell) graph.getDefaultParent();

        graph.getModel().beginUpdate();
        try {
            DAGLayout ctl = new DAGLayout(graph);
            ctl.execute(root);
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void setTreeLayout() {
        mxCell root = (mxCell) graph.getDefaultParent();

        graph.getModel().beginUpdate();
        try {
            mxCompactTreeLayout ctl = new mxCompactTreeLayout(graph);
            ctl.setLevelDistance(100);
            //		ctl.setHorizontal(false);
            ctl.setEdgeRouting(false);
            for (int i = 0; i < root.getChildCount(); i++) {
                ctl.execute(root.getChildAt(i));
            }
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void setCircleLayout() {
        mxCell root = (mxCell) graph.getDefaultParent();

        graph.getModel().beginUpdate();
        try {
            mxCircleLayout ctl = new mxCircleLayout(graph);
            for (int i = 0; i < root.getChildCount(); i++) {
                ctl.execute(root.getChildAt(i));
            }
        } finally {
            graph.getModel().endUpdate();
        }
    }

    public void addResourcePath(ResourcePath parentPath, String resName) {
        // Force to change to data-flow modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).addResourcePath(parentPath, resName);
        model = curStage.getModel();
    }

    public void addChannel(DataTransferChannel channelGen) {
        // Force to change to data-flow modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).addChannel(channelGen);
        model = curStage.getModel();
    }

    public void addIOChannel(DataTransferChannel ioChannelGen) {
        // Force to change to data-flow modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).addIOChannel(ioChannelGen);
        model = curStage.getModel();
    }

    public void addFormulaChannel(FormulaChannel formulaChannelGen) {
        // Force to change to data-flow modeling stage
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (stageChanged) {
            return;
        }
        ((DataFlowModelingStage) curStage).addFormulaChannel(formulaChannelGen);
        model = curStage.getModel();
    }

    public boolean connectEdge(mxCell edge, mxCell src, mxCell dst) {
        boolean stageChanged = changeStage(STAGE_DATA_FLOW_MODELING);
        if (!stageChanged) {
            return false;
        }
        return ((DataFlowModelingStage) curStage).connectEdge(edge, src, dst);
    }

    public mxGraph getGraph() {
        return graph;
    }

    public mxGraphComponent getGraphComponent() {
        return graphComponent;
    }

    public DataTransferModel getModel() {
        model = curStage.getModel();
        return model;
    }

    public Stage getCurStage() {
        return curStage;
    }

    public ArrayList<CompilationUnit> getCodes() {
        return codes;
    }

    public void setCodes(ArrayList<CompilationUnit> codes) {
        this.codes = codes;
    }

    public String getCurFileName() {
        return curFileName;
    }

    public String getCurFilePath() {
        return curFilePath;
    }

    public static class SrcDstAttribute extends EdgeAttribute {
        private final Object src;
        private final Object dst;

        public SrcDstAttribute(Object src, Object dst) {
            this.src = src;
            this.dst = dst;
        }

        public Object getSource() {
            return src;
        }

        public Object getDestination() {
            return dst;
        }

        public String toString() {
            return "";
        }
    }
}
