package ui.controller;

import java.awt.Color;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.imageio.ImageIO;

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.compressors.CompressorOutputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.compress.utils.IOUtils;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.sun.corba.se.spi.ior.Writeable;

import TypeAdapter.AbstractCpsObjectAdapter;
import TypeAdapter.ColorAdapter;
import TypeAdapter.PositionAdapter;

import classes.Category;
import classes.CpsEdge;
import classes.CpsUpperNode;
import classes.AbstractCpsObject;
import classes.HolonElement;
import classes.HolonObject;
import classes.HolonSwitch;
import classes.IdCounter;
import classes.IdCounterElem;
import classes.Position;
import ui.model.Model;

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

	public enum MODE {
		COMPLETE, PARTIAL
	}

	public enum TYPE {
		CATEGORY, CANVAS
	}

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

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

	public enum GRAPHTYPE {
		SWITCH, ELEMENT
	}

	private Model model;
	private Gson gson;
	private int nCat, nObj, nEle, nEdge, nConn, nNodeEdge, nOldEdge, nUnitGraph, nImg;

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

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

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

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

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

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

		addFileToSave(holonFile, stream);

		stream.finish();
		out.close();

	}

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

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

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

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

	/**
	 * Write needed default parameter into the JsonObject. Can be extended later
	 * on
	 * 
	 * @param mode
	 * @param file
	 */
	private void initialize(MODE mode, JsonObject file) {
		// TODO Auto-generated method stub
		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()));
			break;

		default:
			break;
		}
	}

	/**
	 * Store all Categories and Object into a Json File via Serialization
	 * 
	 * @param file
	 */
	private void storeCategory(JsonObject file) {
		// TODO Auto-generated method stub
		// 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 (AbstractCpsObject obj : cat.getObjects()) {
				file.add("CGOBJECT" + getNumerator(NUMTYPE.OBJECT), gson.toJsonTree(obj, AbstractCpsObject.class));
				// if its a holonobject add elements too
				if (obj instanceof HolonObject)
					elementsToJson(TYPE.CATEGORY, file, obj);
			}
		}
	}

	/**
	 * Travers through all Objects via BFS and Stores everything relevant
	 * 
	 * @param file
	 */
	private void storeCanvas(JsonObject file) {
		// TODO Auto-generated method stub
		ArrayDeque<AbstractCpsObject> queue = new ArrayDeque<>();
		AbstractCpsObject u = null;
		// put all objects into queue since there is not starting object
		for (AbstractCpsObject 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, gson.toJsonTree(u, AbstractCpsObject.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 CpsUpperNode) {
				for (AbstractCpsObject adjacent : ((CpsUpperNode) u).getNodes()) {
					queue.add(adjacent);
				}
				// dont forget to add the nodeedges and oldedges
				edgeToJson(EDGETYPE.NODE, file, u.getId(), ((CpsUpperNode) u).getNodeEdges());
				edgeToJson(EDGETYPE.OLD, file, u.getId(), ((CpsUpperNode) u).getOldEdges());
			}
		}
		// lastly add canvasedges into json
		edgeToJson(EDGETYPE.CANVAS, file, 0, model.getEdgesOnCanvas());

	}

	/**
	 * Stores Category or Canvas Elements into Json
	 * 
	 * @param type
	 * @param gson
	 * @param file
	 * @param obj
	 */
	public void elementsToJson(TYPE type, JsonObject file, AbstractCpsObject obj) {
		// TODO Auto-generated method stub
		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", gson.toJsonTree(ele));
			temp.add("ID", new JsonPrimitive(obj.getId()));
			// 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, gson.toJsonTree(temp));
			// if there are gps add them into
			if (ele.getGraphPoints().size() != 0)
				unitgraphToJson(GRAPHTYPE.ELEMENT, file, ele.getId(), ele.getGraphPoints());
			temp = new JsonObject();
		}

	}

	/**
	 * Put the UnitGraphs of Switches or Elements into Json
	 * 
	 * @param ele
	 */
	public void unitgraphToJson(GRAPHTYPE type, JsonObject file, int id, LinkedList<Point> 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;
		default:
			break;
		}
		// add id of element so it can be found again
		temp.add("ID", new JsonPrimitive(id));

		file.add(key, gson.toJsonTree(temp));
	}

	/**
	 * Canvas-Edge, Connections, Node-Edge and Old-Edges to json
	 * 
	 * @param type
	 * @param file
	 * @param id
	 * @param arr
	 */
	public void edgeToJson(EDGETYPE type, JsonObject file, int id, ArrayList<CpsEdge> arr) {
		// TODO Auto-generated method stub
		String k = null;
		boolean b = false;
		JsonObject temp = new JsonObject();

		for (CpsEdge edge : arr) {
			// add properties and only the ids from a and b
			temp.add("properties", gson.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, gson.toJsonTree(temp));
			temp = new JsonObject();
		}

	}

	private void storeData(ArchiveOutputStream stream) throws IOException {
		// TODO Auto-generated method stub
		ArrayList<String> images = new ArrayList<>();
		Files.newDirectoryStream(Paths.get(System.getProperty("user.home") + "/HolonGUI/Images"),
				p -> p.toFile().isFile()).forEach(p -> images.add(p.toString()));
		for (String str : images) {
			File src = new File(str);
			addFileToSave(src, stream);
		}

	}

	private void imageToJson(JsonObject file) {
		// ArrayList<String> images = new ArrayList<>();
		// BufferedImage buffered = null;
		// WritableRaster raster = null;
		// DataBufferByte data = null;
		// String k = null;
		// JsonObject temp = new JsonObject();
		//
		// try {
		// Files.newDirectoryStream(Paths.get(System.getProperty("user.home") +
		// "/HolonGUI/Images"),
		// p -> p.toFile().isFile()).forEach(p -> images.add(p.toString()));
		//
		// for (String str : images) {
		// System.out.println("aaaaa");
		// k = "IMAGE" + getNumerator(NUMTYPE.IMAGE);
		// buffered = ImageIO.read(new File(str));
		// raster = buffered.getRaster();
		// data = (DataBufferByte) raster.getDataBuffer();
		//
		// temp.add("Image", new
		// JsonPrimitive(str.substring(str.indexOf("/HolonGUI/Images"),
		// str.length())));
		// for (int i = 0; i < data.getData().length; i++) {
		// temp.add("" + i, new JsonPrimitive(data.getData()[i]));
		// System.out.println(i);
		// }
		//
		// file.add(k, gson.toJsonTree(temp));
		// }
		// } catch (IOException e) {
		// // TODO Auto-generated catch block
		// e.printStackTrace();
		// }
	}

	private void addFileToSave(File src, ArchiveOutputStream stream) throws IOException {

		String entryName = src.getName();
		ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
		stream.putArchiveEntry(entry);

		BufferedInputStream input = new BufferedInputStream(new FileInputStream(src));

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

	}

	/**
	 * Initialize the Gson with wanted parameters
	 */
	private void initGson() {
		// TODO Auto-generated method stub
		GsonBuilder builder = new GsonBuilder();
		builder.excludeFieldsWithoutExposeAnnotation().serializeNulls().setPrettyPrinting();
		builder.registerTypeAdapter(AbstractCpsObject.class, new AbstractCpsObjectAdapter());
		builder.registerTypeAdapter(Position.class, new PositionAdapter());
		builder.registerTypeAdapter(Color.class, new ColorAdapter());
		// use the builder and make a instance of the Gson
		this.gson = builder.create();
	}

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

	/**
	 * Get the wanted numerator and increment it
	 * 
	 * @param type
	 * @return
	 */
	public 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 IMAGE:
			return nImg++;
		default:
			break;
		}
		return -1;
	}

}