/*******************************************************************************
 * Copyright (c) MOBAC developers
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package mobac.program.atlascreators.impl.rungps;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import mobac.utilities.Utilities;

/**
 * 
 * @author Tom Henne, eSymetric
 */
public class RunGPSAtlasFile {

	public final static String SUFFIX = ".ratlas";
	RandomAccessFile raf;
	RunGPSAtlasHeader rah = new RunGPSAtlasHeader();
	File cacheFile;
	OutputStream cacheOutStream = null;
	long positionInCacheFile = 0L;
	int offset = 0;
	int indexLength = 0;

	public class RAINode {

		RAINode() {
			key = -1;
			value = -1L;
			next = -1;
			child = -1;
			index = -1;
		}

		int key;
		long value;
		int next;
		int child;
		int index;

		void readFrom(int index) throws IOException {
			this.index = index;
			raf.seek(index);
			key = raf.readInt();
			value = raf.readLong();
			next = raf.readInt();
			child = raf.readInt();
		}

		void writeNew() throws IOException {
			raf.seek(indexLength); // go to end
			raf.writeInt(key);
			raf.writeLong(value);
			raf.writeInt(next);
			raf.writeInt(child);
			this.index = indexLength;
			indexLength += 20;
		}

		void write() throws IOException {
			raf.seek(index + 4);
			raf.writeLong(value);
			raf.writeInt(next);
			raf.writeInt(child);
		}

		String listAll() throws IOException {
			return listAll("");
		}

		String listAll(String path) throws IOException {
			List<RAINode> children = getChildren();
			StringBuilder sb = new StringBuilder();
			if (key != -1) { // not for root
				path += "" + key + "/";
				if (value != -1L) {
					sb.append(path).append('=').append(value).append("\n");

				}
			}
			for (RAINode childNode : children) {
				sb.append(childNode.listAll(path));

			}
			return sb.toString();
		}

		List<RAINode> getChildren() throws IOException {
			// currentIndexPos is now at fathers node index pos or -1

			List<RAINode> children = new ArrayList<RAINode>();

			int indexPos = child;

			for (;;) {
				if (indexPos == -1) {
					break;
				}
				RAINode n = new RAINode();
				n.readFrom(indexPos);
				children.add(n);
				indexPos = n.next;
			}

			return children;
		}

		RAINode getChildNode(int key, boolean createNew) throws IOException {
			// currentIndexPos is now at fathers node index pos or -1

			// check if father has a child

			if (this.child == -1) {
				RAINode newNode = new RAINode();
				newNode.key = key;
				newNode.writeNew();

				this.child = newNode.index;
				this.write();
				return newNode;
			}

			// find node on this level

			RAINode n = new RAINode();
			int currentIndexPos = this.child;
			for (;;) {
				if (currentIndexPos == -1) {
					break;
				}
				n.readFrom(currentIndexPos);
				if (n.key == key) {
					return n;
				}
				currentIndexPos = n.next;
			}

			if (!createNew) {
				return null;
			}

			RAINode newNode = new RAINode();
			newNode.key = key;
			newNode.writeNew();

			n.next = newNode.index;
			n.write();
			return newNode;
		}

		public int getSize() {
			return indexLength;
		}

		public RandomAccessFile getRandomAccessFile() {
			return raf;
		}

		RAINode getChildNode(List<Integer> hierarchy) throws IOException {
			if (hierarchy.isEmpty()) {
				return this;

			}
			int key_ = hierarchy.get(0);
			hierarchy.remove(0);

			RAINode childNode = getChildNode(key_, false);
			if (childNode == null) {
				return null;

			} else {
				return childNode.getChildNode(hierarchy);

			}
		}
	}

	public boolean readSuccess() {
		return rah.readSuccess();
	}

	public class RunGPSAtlasHeader {

		public final static long FILETYPE_KEY = 8723487877262773L;
		public final static int HEADER_SIZE = 128; // bytes
		public final static int FILE_VERSION = 3;
		int indexSize; // bytes

		public void writeHeader() throws IOException {
			raf.seek(0);
			raf.writeLong(FILETYPE_KEY);
			raf.writeInt(FILE_VERSION);
			raf.writeInt(indexSize);
		}

		public boolean readHeader() throws IOException {
			if (raf.readLong() != FILETYPE_KEY) {
				return false;

			}
			if (raf.readInt() != FILE_VERSION) {
				return false;

			}
			indexSize = raf.readInt();

			return true;
		}

		public boolean readSuccess() {
			return indexSize > 0;
		}
	}

	public RunGPSAtlasFile(String filePath, boolean write) throws IOException {

		this.offset = RunGPSAtlasHeader.HEADER_SIZE;

		if (write) {
			cacheFile = new File(filePath + ".cac");
			cacheOutStream = new BufferedOutputStream(new FileOutputStream(cacheFile), 8216);

			raf = new RandomAccessFile(filePath, "rw");
			raf.setLength(0); // delete existing content

			indexLength = offset;
			RAINode root = new RAINode();
			root.writeNew();
		} else {
			raf = new RandomAccessFile(filePath, "r");
			rah.readHeader();
		}
	}

	public RAINode getRootNode() throws IOException {
		RAINode root = new RAINode();
		root.readFrom(offset);
		return root;
	}

	public String listAll() throws IOException {
		return getRootNode().listAll();
	}

	public void close() throws IOException {
		if (cacheOutStream != null) {
			cacheOutStream.close();
		}
		if (raf != null) {
			raf.close();
		}
	}

	public void finishArchive() throws IOException {
		cacheOutStream.close();
		cacheOutStream = null;

		FileInputStream fis = new FileInputStream(cacheFile);

		rah.indexSize = indexLength - RunGPSAtlasHeader.HEADER_SIZE;
		rah.writeHeader();

		byte[] buf = new byte[4096];

		raf.seek(indexLength);
		for (;;) {
			int l = fis.read(buf);
			if (l <= 0) {
				break;

			}
			raf.write(buf, 0, l);
		}

		fis.close();

		Utilities.deleteFile(cacheFile);
		close();
	}

	public void addData(List<Integer> hierarchy, byte[] data) throws IOException {
		addData(hierarchy, data, data.length);
	}

	public void addData(List<Integer> hierarchy, byte[] data, int length) throws IOException {
		cacheOutStream.write(data, 0, length);

		List<Integer> posHierarchy = new ArrayList<Integer>();
		posHierarchy.addAll(hierarchy);
		posHierarchy.add(1); // position
		setValue(posHierarchy, positionInCacheFile);

		List<Integer> lenHierarchy = new ArrayList<Integer>();
		lenHierarchy.addAll(hierarchy);
		lenHierarchy.add(2); // size
		setValue(lenHierarchy, length);

		positionInCacheFile += length;

	}

	public static List<Integer> makeHierarchyFromPath(String path) {
		// e.g. /10/3487/8345.png

		// sure that replace() is correct? may be replaceAll() would be better?
		path = path.replace(File.separatorChar, '/');

		int p = path.indexOf('.');
		if (p > 0) {
			path = path.substring(0, p); // remove suffix
		}
		if (path.startsWith("/")) {
			path = path.substring(1);
		}

		try {
			List<Integer> hierarchy = new ArrayList<Integer>();
			for (;;) {
				p = path.indexOf('/');
				if (p > 0) {
					hierarchy.add(Integer.parseInt(path.substring(0, p)));
					path = path.substring(p + 1);
				} else {
					hierarchy.add(Integer.parseInt(path));
					break;
				}
			}

			return hierarchy;
		} catch (NumberFormatException e) {
			return null;
		}

	}

	public byte[] getData(String path) throws IOException {
		return getData(makeHierarchyFromPath(path));
	}

	public byte[] getData(List<Integer> keyHierarchy) throws IOException {
		ArrayList<Integer> posHierarchy = new ArrayList<Integer>();
		posHierarchy.addAll(keyHierarchy);
		posHierarchy.add(1); // position
		long indexPosition = getValue(posHierarchy);
		if (indexPosition == -1) {
			return null;
		}
		ArrayList<Integer> sizeHierarchy = new ArrayList<Integer>();
		sizeHierarchy.addAll(keyHierarchy);
		sizeHierarchy.add(2); // size
		int size = (int) getValue(sizeHierarchy);
		if (size == -1) {
			return null;
		}

		byte[] buf = new byte[size];
		raf.seek(RunGPSAtlasHeader.HEADER_SIZE + rah.indexSize + indexPosition);
		raf.readFully(buf);
		return buf;
	}

	public void setValue(List<Integer> keyHierarchy, long value) throws IOException {
		setValueImpl(getRootNode(), keyHierarchy, value);
	}

	public void setValue(String path, long value) throws IOException {
		setValue(makeHierarchyFromPath(path), value);
	}

	private void setValueImpl(RAINode currentNode, List<Integer> keyHierarchy, long value) throws IOException {
		int key = keyHierarchy.get(0);
		keyHierarchy.remove(0);
		RAINode n = currentNode.getChildNode(key, true);
		if (keyHierarchy.isEmpty()) {
			n.value = value;
			n.write();
		} else {
			setValueImpl(n, keyHierarchy, value);
		}
	}

	public long getValue(List<Integer> keyHierarchy) throws IOException {
		RAINode n = getRootNode().getChildNode(keyHierarchy);
		return n == null ? -1L : n.value;
	}

	public long getValue(String path) throws IOException {
		return getValue(makeHierarchyFromPath(path));
	}

	public void setString(List<Integer> keyHierarchy, String value) throws IOException {
		byte[] data = value.getBytes(StandardCharsets.UTF_8);
		addData(keyHierarchy, data);
	}

	public void setString(String path, String value) throws IOException {
		setString(makeHierarchyFromPath(path), value);
	}

	public String getString(List<Integer> keyHierarchy) throws IOException {
		byte[] data = getData(keyHierarchy);
		if (data != null) {
			return new String(data, StandardCharsets.UTF_8);
		}
		return null;
	}

	public String getString(String path) throws IOException {
		return getString(makeHierarchyFromPath(path));
	}

}
