package ui.controller;

import classes.*;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.utils.IOUtils;
import ui.model.Model;

import java.awt.*;
import java.awt.geom.Point2D;
import java.io.*;
import java.util.*;
import java.util.List;

/**
 * Controller for Storage.
 *
 * @author Gruppe14
 */
public class SaveController {

    private Model model;
    private int nCat, nObj, nEle, nEdge, nConn, nNodeEdge, nOldEdge, nUnitGraph, nStatsGraph;

    /**
     * Constructor.
     *
     * @param model the Model
     */
    SaveController(Model model) {
        this.model = model;
    }

    /**
     * Writes the current State of the Modelling into a JSON File which can be
     * loaded.
     *
     * @param path the Path
     * @throws IOException exception
     */
    void writeSave(String path) throws IOException, ArchiveException {

        File dst = new File(path);
        File holonFile = File.createTempFile("tmp", ".json");
        holonFile.deleteOnExit();
        dst.delete();

        OutputStream output = new FileOutputStream(dst);
        ArchiveOutputStream stream = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.ZIP,
                output);

        initNumeration();
        JsonObject file = new JsonObject();
        initialize(MODE.COMPLETE, file);
        storeCategory(file);
        storeCanvas(file);
        storeStatistics(file);
        storeData(stream);

        FileWriter writer = new FileWriter(holonFile);
        writer.write(model.getGson().toJson(file));
        writer.flush();
        writer.close();

        addFileToSave(holonFile, stream, false);

        stream.finish();
        output.close();
    }

    /**
     * Writes the windows status (its position and dimensions)
     *
     * @param path the Path where to save it
     */
    void writeWindowStatus(String path, int x, int y, int width, int height)
            throws IOException, ArchiveException {
        JsonObject file = new JsonObject();
        initialize(MODE.SIZE, file);
        storeWindowPosAndSize(file, x, y, width, height);

        FileWriter writer = new FileWriter(path);
        writer.write(model.getGson().toJson(file));
        writer.flush();
        writer.close();
    }

    /**
     * Writes the Autosave File.
     *
     * @param path the Path
     * @throws IOException Exception
     */
    void writeAutosave(String path) throws IOException {

        initNumeration();
        JsonObject file = new JsonObject();
        initialize(MODE.PARTIAL, file);
        storeCanvas(file);

        FileWriter writer = new FileWriter(path);
        writer.write(model.getGson().toJson(file));

        writer.flush();
        writer.close();
    }

    /**
     * Writes the Category File in Case of Changes
     */
    void writeCategory(String path) throws IOException {

        initNumeration();
        JsonObject file = new JsonObject();
        initialize(MODE.CATEGORY, file);
        storeCategory(file);

        FileWriter writer = new FileWriter(path);
        writer.write(model.getGson().toJson(file));

        writer.flush();
        writer.close();

    }

    /**
     * Write needed default parameter into the JsonObject. Can be extended later
     * on
     */
    private void initialize(MODE mode, JsonObject file) {
        switch (mode) {
            case COMPLETE:
                file.add("MODE", new JsonPrimitive(mode.name()));
                file.add("IDCOUNTER", new JsonPrimitive(IdCounter.getCounter()));
                file.add("IDCOUNTERELEMENT", new JsonPrimitive(IdCounterElem.getCounter()));
                file.add("CANVAS_SIZE_X", new JsonPrimitive(model.getCanvasX()));
                file.add("CANVAS_SIZE_Y", new JsonPrimitive(model.getCanvasY()));
                break;
            case PARTIAL:
                file.add("MODE", new JsonPrimitive(mode.name()));
                file.add("IDCOUNTER", new JsonPrimitive(IdCounter.getCounter()));
                file.add("IDCOUNTERELEMENT", new JsonPrimitive(IdCounterElem.getCounter()));
                file.add("CANVAS_SIZE_X", new JsonPrimitive(model.getCanvasX()));
                file.add("CANVAS_SIZE_Y", new JsonPrimitive(model.getCanvasY()));
                break;
            case CATEGORY:
                file.add("MODE", new JsonPrimitive(mode.name()));
                break;
            case SIZE:
                file.add("MODE", new JsonPrimitive(mode.name()));
            default:
                break;
        }

    }

    /**
     * Store all Categories and Object into a Json File via Serialization
     */
    private void storeCategory(JsonObject file) {
        // forall categories store them into the jsontree
        for (Category cat : model.getCategories()) {
            String key = "CATEGORY" + getNumerator(NUMTYPE.CATEGORY);

            file.add(key, new JsonPrimitive(cat.getName()));
            // forall object in the category store them into the jsontree
            for (AbstractCanvasObject obj : cat.getObjects()) {
                file.add("CGOBJECT" + getNumerator(NUMTYPE.OBJECT),
                        model.getGson().toJsonTree(obj, AbstractCanvasObject.class));
                // if its a holonobject add elements too
                if (obj instanceof HolonObject)
                    elementsToJson(TYPE.CATEGORY, file, obj);
            }
        }
    }

    private void storeWindowPosAndSize(JsonObject file, int x, int y, int width, int height) {
        file.add("POS_X", new JsonPrimitive(x));
        file.add("POS_Y", new JsonPrimitive(y));
        file.add("WIDTH", new JsonPrimitive(width));
        file.add("HEIGHT", new JsonPrimitive(height));

    }

    /**
     * Travers through all Objects via BFS and Stores everything relevant
     */
    private void storeCanvas(JsonObject file) {
        ArrayDeque<AbstractCanvasObject> queue = new ArrayDeque<>();
        AbstractCanvasObject u = null;
        // put all objects into queue since there is not starting object
        for (AbstractCanvasObject cps : model.getObjectsOnCanvas()) {
            queue.add(cps);
        }
        // while quene not empty
        while (!queue.isEmpty()) {

            // u = current node
            u = queue.pop();
            // add currentnode into jsontree
            String key = "CVSOBJECT" + getNumerator(NUMTYPE.OBJECT);
            file.add(key, model.getGson().toJsonTree(u, AbstractCanvasObject.class));
            // and its connections too
            edgeToJson(EDGETYPE.CONNECTION, file, u.getId(), u.getConnections());
            // if holonobject elements too
            if (u instanceof HolonObject)
                elementsToJson(TYPE.CANVAS, file, u);
            // if switch graphpoints too
            if (u instanceof HolonSwitch)
                if (((HolonSwitch) u).getGraphPoints().size() != 0)
                    unitgraphToJson(GRAPHTYPE.SWITCH, file, u.getId(), ((HolonSwitch) u).getGraphPoints());
            // if uppernode put all nodes inside the uppernode into queue
            if (u instanceof GroupNode) {
                for (AbstractCanvasObject adjacent : ((GroupNode) u).getNodes()) {
                    queue.add(adjacent);
                }

            }
        }
        // lastly add canvasedges into json
        edgeToJson(EDGETYPE.CANVAS, file, 0, model.getEdgesOnCanvas());

    }

    private void storeStatistics(JsonObject file) {
        trackedToJson(file);
        statisticgraphToJson(file);
    }

    /**
     * Save wanted Data
     */
    private void storeData(ArchiveOutputStream stream) throws IOException {
        File images = new File(System.getProperty("user.home") + "/.config/HolonGUI/Images");
        File background = new File(System.getProperty("user.home") + "/.config/HolonGUI/BackgroundImages");
        addFilesToSave(images, stream);
        addFilesToSave(background, stream);

    }

    /**
     * Stores Category or Canvas Elements into Json
     */
    void elementsToJson(TYPE type, JsonObject file, AbstractCanvasObject obj) {
        JsonObject temp = new JsonObject();
        String key = null;
        // forall elements store them into json and include the id of the object
        for (HolonElement ele : ((HolonObject) obj).getElements()) {
            temp.add("properties", model.getGson().toJsonTree(ele));
            temp.add("ID", new JsonPrimitive(obj.getId()));
            temp.add("FlexList", model.getGson().toJsonTree(ele.flexList,  new TypeToken<List<Flexibility>>() {
            }.getType()));
            // switch for deciding the key
            switch (type) {
                case CANVAS:
                    key = "CVSELEMENT" + getNumerator(NUMTYPE.ELEMENT);
                    break;
                case CATEGORY:
                    key = "CGELEMENT" + getNumerator(NUMTYPE.ELEMENT);
                    break;
                default:
                    break;
            }
            file.add(key, model.getGson().toJsonTree(temp));
            // if there are gps add them into
            if (!ele.getGraphPoints().isEmpty())
            	unitgraphTESTToJson(file, ele.getId(), ele.getGraphPoints());
            temp = new JsonObject();
        }

    }

    /**
     * Put the UnitGraphs of Switches or Elements into Json
     */
    void unitgraphToJson(GRAPHTYPE type, JsonObject file, int id, LinkedList<Point2D.Double> graph) {

        JsonObject temp = new JsonObject();
        String key = null;
        // forall points add them
        for (int i = 0; i < graph.size(); i++) {
            temp.add("" + i, new JsonPrimitive(graph.get(i).x + ":" + graph.get(i).y));
        }
        // decide key
        switch (type) {
            case SWITCH:
                key = "SWUNITGRAPH" + getNumerator(NUMTYPE.UNITGRAPH);
                break;
            case ELEMENT:
                key = "ELEUNITGRAPH" + getNumerator(NUMTYPE.UNITGRAPH);
                break;
            case TESTELEMENT:
                key = "ELETESTUNITGRAPH" + getNumerator(NUMTYPE.UNITGRAPH);
                break;
            default:
                break;
        }
        // add id of element so it can be found again
        temp.add("ID", new JsonPrimitive(id));

        file.add(key, model.getGson().toJsonTree(temp));
    }
    /**
     * Put the UnitGraphs of Switches or Elements into Json
     */
    void unitgraphTESTToJson(JsonObject file, int id, LinkedList<Point2D.Double> graph) {

        JsonObject temp = new JsonObject();
        String key = null;
        // forall points add them
        for (int i = 0; i < graph.size(); i++) {
            temp.add("" + i, new JsonPrimitive(graph.get(i).x + ":" + graph.get(i).y));
        }
        key = "ELETESTUNITGRAPH" + getNumerator(NUMTYPE.UNITGRAPH);
        // add id of element so it can be found again
        temp.add("ID", new JsonPrimitive(id));
        file.add(key, model.getGson().toJsonTree(temp));
    }

    /**
     * Canvas-Edge, Connections, Node-Edge and Old-Edges to json
     */
    private void edgeToJson(EDGETYPE type, JsonObject file, int id, ArrayList<Edge> arr) {
        String k = null;
        boolean b = false;
        JsonObject temp = new JsonObject();

        for (Edge edge : arr) {
            // add properties and only the ids from a and b
            temp.add("properties", model.getGson().toJsonTree(edge));
            temp.add("A", new JsonPrimitive(edge.getA().getId()));
            temp.add("B", new JsonPrimitive(edge.getB().getId()));

            // Key and occasionally the id of Uppernode
            switch (type) {
                case CANVAS:
                    k = "CVSEDGE" + getNumerator(NUMTYPE.EDGE);
                    break;
                case CONNECTION:
                    k = "CONNEDGE" + getNumerator(NUMTYPE.CONNECTION);
                    break;
                case NODE:
                    temp.add("ID", new JsonPrimitive(id));
                    k = "NODEEDGE" + getNumerator(NUMTYPE.NODEEDGE);
                    break;
                case OLD:
                    temp.add("ID", new JsonPrimitive(id));
                    k = "OLDEDGE" + getNumerator(NUMTYPE.OLDEDGE);
                    break;
                default:
                    break;
            }
            // lookup if the CVS, NODE or OLDEDGE are also connections
            if (edge.getA().getConnections().contains(edge) && edge.getA().getConnections().contains(edge)
                    && !type.equals(EDGETYPE.CONNECTION))
                b = true;
            temp.add("connection", new JsonPrimitive(b));
            file.add(k, model.getGson().toJsonTree(temp));
            temp = new JsonObject();
        }

    }

    private void trackedToJson(JsonObject file) {
        JsonObject temp = new JsonObject();
        String key = "TRACKED";
        // forall points add them
        for (int i = 0; i < model.getTrackingObj().size(); i++) {
            temp.add("" + i, new JsonPrimitive(model.getTrackingObj().get(i).getId()));
        }

        file.add(key, model.getGson().toJsonTree(temp));
    }

    private void statisticgraphToJson(JsonObject file) {
        JsonObject temp = new JsonObject();

        List<String> keys = new ArrayList<>(model.getGraphTable().keySet());

        for (String k : keys) {
            JsonObject dataSet = new JsonObject();
            for (int i = 0; i < model.getGraphTable().get(k).getStatGraph().getDataSets().size(); i++) {
                TrackedDataSet set = model.getGraphTable().get(k).getStatGraph().getDataSets().get(i);
                AbstractCanvasObject cps = set.getCpsObject();

                dataSet.add("ID", (cps == null ? null : new JsonPrimitive(cps.getId())));
                dataSet.add("COLOR", model.getGson().toJsonTree(set.getColor(), Color.class));
                dataSet.add("PROPERTY", new JsonPrimitive(set.getProperty()));
                temp.add("" + i, model.getGson().toJsonTree(dataSet));
                dataSet = new JsonObject();
            }
            temp.add("KEY", new JsonPrimitive(k));

            file.add("STATSGRAPH" + getNumerator(NUMTYPE.STATSGRAPH), model.getGson().toJsonTree(temp));
            temp = new JsonObject();
        }

    }

    /**
     * Differs Between Single file or whole Directory to Store
     */
    private void addFilesToSave(File src, ArchiveOutputStream stream) throws IOException {
        if (!src.exists())
            return;

        ArrayList<File> files = new ArrayList<>();
        files.addAll(Arrays.asList(src.listFiles()));

        for (File file : files) {
            if (file.isDirectory())
                addFilesToSave(file, stream);
            else
                addFileToSave(file, stream, true);
        }

    }

    /**
     * Add a File into the Archive
     */
    private void addFileToSave(File src, ArchiveOutputStream stream, boolean dir) throws IOException {
        String entryName = (dir ? src.getCanonicalPath() : src.getName());

        entryName = checkOS(entryName);

        ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
        stream.putArchiveEntry(entry);
        BufferedInputStream input = new BufferedInputStream(new FileInputStream(src));

        IOUtils.copy(input, stream);
        input.close();
        stream.closeArchiveEntry();
    }

    private String checkOS(String entryName) {
        String os = System.getProperty("os.name").toLowerCase();
        String ret = entryName;
        String partition = null;

        if (os.contains("windows")) {
            partition = ret.substring(0, ret.indexOf(":") + 1);
            ret = ret.replace(System.getProperty("user.home") + "\\.config\\HolonGUI\\", "");
            ret = ret.replace(partition, "");
        }
        if (os.contains("mac")) {
            // dosmth
            ret = ret.replace(System.getProperty("user.home") + "/.config/HolonGUI/", "");
        }
        if (os.contains("linux")) {
            // dosmth
            ret = ret.replace(System.getProperty("user.home") + "/.config/HolonGUI/", "");
        }
        if (os.contains("solaris")) {
            // dosmth
        }
        return ret;
    }

    /**
     * Just initialize the Numerators for the Json Keys. Maybe bad Style..
     */
    void initNumeration() {
        this.nCat = this.nObj = this.nEle = this.nEdge = this.nConn = this.nNodeEdge = this.nOldEdge = this.nStatsGraph = 0;
    }

    /**
     * Get the wanted numerator and increment it
     */
    int getNumerator(NUMTYPE type) {

        switch (type) {
            case CATEGORY:
                return nCat++;
            case OBJECT:
                return nObj++;
            case ELEMENT:
                return nEle++;
            case EDGE:
                return nEdge++;
            case CONNECTION:
                return nConn++;
            case NODEEDGE:
                return nNodeEdge++;
            case OLDEDGE:
                return nOldEdge++;
            case UNITGRAPH:
                return nUnitGraph++;
            case STATSGRAPH:
                return nStatsGraph++;

            default:
                break;
        }
        return -1;
    }

    public enum MODE {
        COMPLETE, PARTIAL, CATEGORY, SIZE,
    }

    public enum TYPE {
        CATEGORY, CANVAS
    }

    public enum EDGETYPE {
        CANVAS, CONNECTION, NODE, OLD, LAYER
    }

    public enum NUMTYPE {
        CATEGORY, OBJECT, ELEMENT, EDGE, CONNECTION, NODEEDGE, OLDEDGE, UNITGRAPH, STATSGRAPH
    }

    public enum GRAPHTYPE {
        SWITCH, ELEMENT, TESTELEMENT
    }

}