package ui.controller;

import classes.*;
import model.Model;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.utils.IOUtils;

import java.awt.geom.Point2D;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Controller for the Loading.
 *
 * @author Gruppe14
 */
public class LoadController {
    private Model model;
    private CategoryController cgC;
    private CanvasController cvsC;
    private ObjectController objC;
    private NodeController uppC;
    private MultiPurposeController mpC;
    private JsonParser parser;
    private static HashMap<String,String> OldTypeNameReplacement = new HashMap<String, String>();
    
    static {
    	OldTypeNameReplacement.put("CpsNode", "Node");
    	OldTypeNameReplacement.put("CpsUpperNode", "GroupNode");
    	
    }
    /**
     * Constructor.
     *
     * @param model Model
     * @param cg    CategoryController
     * @param cvs   CanvasController
     * @param obj   ObjectController
     * @param mp    MultiPurposeController
     */
    LoadController(Model model, CategoryController cg, CanvasController cvs, ObjectController obj,
                   NodeController uppC, MultiPurposeController mp) {
    	
        this.model = model;
        this.cgC = cg;
        this.cvsC = cvs;
        this.objC = obj;
        this.uppC = uppC;
        this.mpC = mp;
        this.parser = new JsonParser();
    }

    /**
     * Reads the the JSON File and load the state into the Model.
     *
     * @param path the Path
     * @throws IOException exception
     */
    void readSave(String path) throws IOException, ArchiveException {

        File src = new File(path);
        File folder = readArchive(src);
        folder.deleteOnExit();
        String trim = folder.getPath().substring(0,
                folder.getPath().lastIndexOf(folder.getName()) + folder.getName().length());

        forwardFiles(folder, trim);

    }

    /**
     * reads the dimensions file containing the saved position and dimensions of the frame
     *
     * @return a list of the form [x, y, width, height]
     */
    ArrayList<Integer> readWindowDimensions(String path) throws FileNotFoundException {
        JsonObject json = (JsonObject) parser.parse(new FileReader(path));
        ArrayList<Integer> dimensions = new ArrayList<>();

        List<String> keys = getKeys(json);
        for (int i = 1; i < 5; i++) {
            dimensions.add(json.get(keys.get(i)).getAsInt());
        }

        return dimensions;
    }

    void readJson(String path) throws IOException {
        JsonObject json = (JsonObject) parser.parse(new FileReader(path));
        // get all keys via stream
        List<String> keys = getKeys(json);
        List<String> edges = keys.stream().filter(key -> key.contains("EDGE"))
                .collect(Collectors.toCollection(ArrayList::new));

        HashMap<Integer, AbstractCanvasObject> objDispatch = new HashMap<>();
        HashMap<Integer, HolonElement> eleDispatch = new HashMap<>();

        initialize(json);
        forwardObjects(keys, json, objDispatch, eleDispatch);
        forwardEdges(edges, json, objDispatch);

    }

    /**
     * Loads the Files from the Savefile
     *
     * @param trim the part of the file's path to be trimmed
     * @throws IOException if anythings goes wrong reading the files
     */
    private void forwardFiles(File folder, String trim) throws IOException {
        for (File file : folder.listFiles()) {
            File dst = new File(
                    System.getProperty("user.home") + "/.config/HolonGUI/" + file.getPath().replace(trim, ""));

            if (file.getName().contains(".json"))
                readJson(file.getPath());
            else if (file.isDirectory())
                forwardFiles(file, trim);
            else {
                dst.getParentFile().mkdirs();
                Files.copy(file.toPath(), dst.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }

        }
    }

    /**
     * distribute the Edges
     */
    private void forwardEdges(List<String> edges, JsonObject json, HashMap<Integer, AbstractCanvasObject> objDispatch) {
        for (String edge : edges) {
            if (edge.contains("CVSEDGE"))
                loadEdge(EDGETYPE.CANVAS, json.get(edge), objDispatch);
            if (edge.contains("CONNEDGE"))
            	loadEdge(EDGETYPE.CONNECTION, json.get(edge), objDispatch);
            if (edge.contains("NODE"))
                loadEdge(EDGETYPE.NODE, json.get(edge), objDispatch);
            if (edge.contains("OLD"))
                loadEdge(EDGETYPE.OLD, json.get(edge), objDispatch);
        }

    }

    /**
     * Distribute the given keys for right processing
     */
    private void forwardObjects(List<String> keys, JsonObject json, HashMap<Integer, AbstractCanvasObject> objDispatch,
                                HashMap<Integer, HolonElement> eleDispatch) {
        for (String key : keys) {
            if (key.contains("CATEGORY"))
                loadCategory(json.get(key));
            if (key.contains("CGOBJECT"))
                loadCategoryObject(json.get(key).getAsJsonObject());
            if (key.contains("CGELEMENT"))
                loadCategoryElements(json.get(key), eleDispatch);//TODO
            if (key.contains("CVSOBJECT"))
                loadCanvasObject(json.get(key).getAsJsonObject(), objDispatch);
            if (key.contains("CVSELEMENT"))
                loadCanvasElements(json.get(key), objDispatch, eleDispatch);
            if (key.contains("SWUNITGRAPH"))
                loadUnitGraph(GRAPHTYPE.SWITCH, json.get(key), objDispatch, null);
            if (key.contains("ELEUNITGRAPH"))
                loadUnitGraph(GRAPHTYPE.ELEMENT, json.get(key), null, eleDispatch);
            if (key.contains("ELETESTUNITGRAPH"))
                loadUnitGraph(GRAPHTYPE.TESTELEMENT, json.get(key), null, eleDispatch);
            if (key.contains("TRACKED"))
                loadTracked(json.get(key), objDispatch);
        }

    }

    /**
     * Init the Global Parameters
     */
    private void initialize(JsonObject json) {

        switch (MODE.valueOf(json.get("MODE").getAsString())) {
            case COMPLETE:
                model.setCvsObjIdx(new HashMap<>());
                model.setObjectsOnCanvas(new ArrayList<>());
                model.setEdgesOnCanvas(new ArrayList<>());
                model.setHashcodeMap(new HashMap<>());
                model.setCanvasX(json.get("CANVAS_SIZE_X").getAsInt());
                model.setCanvasX(json.get("CANVAS_SIZE_Y").getAsInt());
                IdCounter.setCounter(json.get("IDCOUNTER").getAsInt());
                IdCounterElem.setCounter(json.get("IDCOUNTERELEMENT").getAsInt());
                break;
            case PARTIAL:
                model.setCvsObjIdx(new HashMap<>());
                model.setObjectsOnCanvas(new ArrayList<>());
                model.setEdgesOnCanvas(new ArrayList<>());
                model.setHashcodeMap(new HashMap<>());
                model.setCanvasX(json.get("CANVAS_SIZE_X").getAsInt());
                model.setCanvasX(json.get("CANVAS_SIZE_Y").getAsInt());
                IdCounter.setCounter(json.get("IDCOUNTER").getAsInt());
                IdCounterElem.setCounter(json.get("IDCOUNTERELEMENT").getAsInt());
                break;
            case CATEGORY:
                model.setCgIdx(new HashMap<>());
                model.setCategories(new ArrayList<>());

            default:
                break;
        }

    }

    /**
     * Load a given Category
     */
    private void loadCategory(JsonElement jsonElement) {
        if (mpC.searchCat(jsonElement.getAsString()) == null)
            cgC.addCategory(new Category(jsonElement.getAsString()));
    }

    /**
     * Load a given Object in Category by Deserialization
     */
    private void loadCategoryObject(JsonObject jsonObject) {
    	AbstractCanvasObject temp;
    	replaceOldClassNames(jsonObject);
    	try {
    		temp = model.getGson().fromJson(jsonObject, AbstractCanvasObject.class);
    	}catch (com.google.gson.JsonParseException e){
    		System.err.println(jsonObject.get("type").getAsString());
    		return;
    	}
        
        temp.setImage(checkOS(temp.getImage()));
        initObjects(temp);
        if (mpC.searchCatObj(mpC.searchCat(temp.getSav()), temp.getObjName()) != null)
            cgC.deleteObject(temp.getSav(), temp.getObjName());
        cgC.addObject(mpC.searchCat(temp.getSav()), temp);

    }

    /**
     * Load a given Element in Category by Deserialization
     * @param eleDispatch 
     */
    private void loadCategoryElements(JsonElement jsonElement, HashMap<Integer, HolonElement> eleDispatch) {
        HolonElement temp = model.getGson().fromJson(jsonElement.getAsJsonObject().get("properties").getAsJsonObject(),
                HolonElement.class);
        eleDispatch.put(temp.getId(), temp);
        initElements(temp);
        objC.addElementIntoCategoryObject(temp.getSaving().getKey(), temp.getSaving().getValue(), temp);
    }

    /**
     * Load a given Object in Canvas by Deserialization
     */
    private void loadCanvasObject(JsonObject jsonObject, HashMap<Integer, AbstractCanvasObject> objDispatch) {
    	AbstractCanvasObject temp = null;
    	replaceOldClassNames(jsonObject);
    	try {
    		temp = model.getGson().fromJson(jsonObject, AbstractCanvasObject.class);
    	}catch (com.google.gson.JsonParseException e){
    		System.err.println(jsonObject.get("type").getAsString());
    		return;
    	}
        initObjects(temp);
        temp.setImage(checkOS(temp.getImage()));
        if (temp instanceof GroupNode) {
            model.getHashcodeMap().put(jsonObject.get("hash").getAsInt(), (GroupNode) temp);
            ((GroupNode) temp).setLeftBorder(jsonObject.get("properties").getAsJsonObject().get("leftBorder").getAsInt());
        }
        // if its stored before on the canvas just put it there
        if (temp.getSav().equals("CVS")) {
            cvsC.addObject(temp, false);

        } else {
            // else look up the table and put it into the right Uppernode
            GroupNode temp2 = (GroupNode) objDispatch.get(Integer.parseInt(temp.getSav()));
            if(temp2 != null)
            	uppC.addObjectInUpperNode(temp, temp2, false);
            else{
            	/**
            	 * if null try to load on canvas
            	 */
            	cvsC.addObject(temp, false);
            }

       }

        objDispatch.put(temp.getId(), temp);

    }

    
    
	private void replaceOldClassNames(JsonObject abstractCanvasClassJsonObject) {
		String type = abstractCanvasClassJsonObject.get("type").getAsString();
		if(OldTypeNameReplacement.containsKey(type)) {
			abstractCanvasClassJsonObject.addProperty("type", OldTypeNameReplacement.get(type));
		}
	}

    /**
     * Load a given Element in Canvas by Deserialization
     */
    private void loadCanvasElements(JsonElement jsonElement, HashMap<Integer, AbstractCanvasObject> objDispatch,
                                    HashMap<Integer, HolonElement> eleDispatch) {

        JsonObject object = jsonElement.getAsJsonObject();
        HolonElement ele = model.getGson().fromJson(object.get("properties"), HolonElement.class);
        initElements(ele);
        ele.flexList = model.getGson().fromJson(object.get("FlexList"), new TypeToken<List<Flexibility>>() {}.getType());
        //object.get("FlexList").getAsJsonArray().forEach(jo -> System.out.println("Hallo"));
        //object.get("FlexList").getAsJsonArray().forEach(flexJson -> flexJson.getAsJsonObject().get("constrainList").getAsJsonArray().forEach(constrainJson -> System.out.println("Constrain:" + constrainJson.getAsJsonObject().get("name").getAsString())));
        ele.flexList.stream().forEach(flex -> {
        	flex.setElement(ele);
        	flex.constrainList.forEach(con -> con.fixJson());
        	
        });
        
        
        // id which Object it was stored before
        int stored = object.get("ID").getAsInt();
        // lookup that object
        HolonObject hObject = (HolonObject) objDispatch.get(stored);
        // add it
        objC.addElement(hObject, ele);
        ele.parentObject = hObject;
        // store element also inside a table
        eleDispatch.put(ele.getId(), ele);
    }

    /**
     * Load a given Edge by Deserialization
     */
    private void loadEdge(EDGETYPE type, JsonElement jsonElement, HashMap<Integer, AbstractCanvasObject> objDispatch) {
        JsonObject object = jsonElement.getAsJsonObject();
        Edge temp = model.getGson().fromJson(object.get("properties"), Edge.class);
        initCpsEdge(temp);
        // look for A and B inside the Table
        temp.setA(objDispatch.get(object.get("A").getAsInt()));
        temp.setB(objDispatch.get(object.get("B").getAsInt()));
        model.getEdgesOnCanvas().add(temp);
    }

    /**
     * Load a Unitgraph by Deserialization
     */
    private void loadUnitGraph(GRAPHTYPE type, JsonElement jsonElement, HashMap<Integer, AbstractCanvasObject> objDispatch,
                               HashMap<Integer, HolonElement> eleDispatch) {
    	//TODO Make UnitGraph unterscheidung hinf�llig!!!!
        JsonObject object = jsonElement.getAsJsonObject();
        List<String> keys = getKeys(object);
        String p;
        int mid;
        int sav = 0;
     	LinkedList<Point2D.Double> graphpointTEST = new LinkedList<>();
        for (String k : keys) {
            if (!k.equals("ID")) {
                p = object.get(k).getAsString();
                mid = p.indexOf(':');
                double x1 = Double.parseDouble(p.substring(0, mid));
                double y1 = Double.parseDouble(p.substring(mid + 1, p.length()));
                graphpointTEST.add(new Point2D.Double(x1, y1));
            } else
                // else its an ID
                sav = object.get(k).getAsInt();

        }  
        
       

        switch (type) {
            case SWITCH:
                HolonSwitch sw = (HolonSwitch) objDispatch.get(sav);
                sw.setGraphPoints(graphpointTEST);
                sw.sampleGraph();
                break;
            case ELEMENT:
            	System.out.println("Write me new");
            	break;
            case TESTELEMENT:
                HolonElement ele1 = eleDispatch.get(sav);
                ele1.setGraphPoints(graphpointTEST);
                ele1.sampleGraph();
                break;
            default:
                break;
        }
    }

    private void loadTracked(JsonElement jsonElement, HashMap<Integer, AbstractCanvasObject> objDispatch) {
        JsonObject object = jsonElement.getAsJsonObject();
        List<String> keys = getKeys(object);

        for (String k : keys) {
            int id = object.get(k).getAsInt();
            if (objDispatch.get(id) instanceof HolonObject) {
                ((HolonObject) objDispatch.get(id)).updateTrackingInfo();
            }
        }

    }

    private File readArchive(File src) throws IOException, ArchiveException {
        File tmp = Files.createTempDirectory("tmpHolon").toFile();
        tmp.deleteOnExit();
        InputStream input = new FileInputStream(src);
        ArchiveInputStream stream = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.ZIP,
                input);

        ArchiveEntry entry = stream.getNextEntry();
        while (entry != null) {
            // String entryName = checkOS(entry.getName());
            File file = new File(tmp, entry.getName());
            file.getParentFile().mkdirs();
            OutputStream output = new FileOutputStream(file);
            IOUtils.copy(stream, output);
            output.close();
            // file.createNewFile();
            entry = stream.getNextEntry();
        }

        stream.close();
        input.close();

        return tmp;
    }

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

        if (!ret.contains("HolonGUI"))
            return ret;

        if (os.contains("windows")) {
            ret = ret.replace("/", "\\");
            ret = System.getProperty("user.home") + "\\.config"
                    + ret.substring(ret.indexOf("\\HolonGUI\\"), ret.length());

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

    /**
     * Init new Arrays which haven't been serialized along the object
     */
    void initObjects(AbstractCanvasObject obj) {

        obj.setConnections(new ArrayList<>());

        if (obj instanceof HolonObject) {
            ((HolonObject) obj).setElements(new ArrayList<>());
        }

        if (obj instanceof HolonSwitch) {
           
            ((HolonSwitch) obj).setGraphPoints(new LinkedList<>()); 
            ((HolonSwitch) obj).reset();
            ((HolonSwitch) obj).sampleGraph();
        }

        if (obj instanceof GroupNode) {
            ((GroupNode) obj).setNodes(new ArrayList<>());
            ((GroupNode) obj).setNodesIdx(new HashMap<>());
        }
    }

    /**
     * Init Elements (set available energy, set new graph points)
     *
     * @param ele the element to be initialized
     */
    void initElements(HolonElement ele) {
        ele.flexList = new ArrayList<Flexibility>();
        ele.setGraphPoints(new LinkedList<>());
        ele.reset();
    }

    /**
     * Init Edges (set tags and reset source and target)
     *
     * @param edge the edge to be initialized
     */
    void initCpsEdge(Edge edge) {
        edge.setTags(new ArrayList<>());
        edge.setA(null);
        edge.setB(null);
    }

    /**
     * Get Set of Keys
     *
     * @return the keys from the json object
     */
    List<String> getKeys(JsonObject json) {
        return json.entrySet().stream().map(i -> i.getKey()).collect(Collectors.toCollection(ArrayList::new));
    }

    /**
     * enum Mode. (in SaveController there is an additional mode called SIZE,
     * it is not currently needed here and was therefore not added)
     */
    public enum MODE {
        COMPLETE, PARTIAL, CATEGORY
    }

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

    public enum GRAPHTYPE {
        SWITCH, ELEMENT, TESTELEMENT
    }

}
