/*
 * Copyright (C) 2012, Gerd Petermann
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * 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.
 */

package uk.me.parabola.splitter.parser;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;

import uk.me.parabola.splitter.Area;
import uk.me.parabola.splitter.Element;
import uk.me.parabola.splitter.MapProcessor;
import uk.me.parabola.splitter.Node;
import uk.me.parabola.splitter.Relation;
import uk.me.parabola.splitter.Utils;
import uk.me.parabola.splitter.Way;

/**
 * Parser for the o5m format described here: http://wiki.openstreetmap.org/wiki/O5m
 * The routines to are based on the osmconvert.c source from Markus Weber who allows 
 * to copy them for any o5m IO, thanks a lot for that. 
 * @author GerdP  
 *
 */
public class O5mMapParser {
	// O5M data set constants
	private static final int NODE_DATASET = 0x10;
	private static final int WAY_DATASET = 0x11;
	private static final int REL_DATASET = 0x12;
	private static final int BBOX_DATASET = 0xdb;
	private static final int TIMESTAMP_DATASET = 0xdc;
	private static final int HEADER_DATASET = 0xe0;
	private static final int EOD_FLAG = 0xfe;
	private static final int RESET_FLAG = 0xff;
	
	// o5m constants
	private static final int STRING_TABLE_SIZE = 15000;
	private static final int MAX_STRING_PAIR_SIZE = 250 + 2;
	private static final String[] REL_REF_TYPES = {"node", "way", "relation", "?"};
	private static final double FACTOR = 1d / 1000000000; // used with 100*<Val>*FACTOR 
	
	// for status messages
	private final ElementCounter elemCounter = new ElementCounter();
	// flags set by the processor to signal what information is not needed
	private final boolean skipTags;
	private final boolean skipNodes;
	private final boolean skipWays;
	private final boolean skipRels;

	private final FileChannel fileChannel;
	// Buffer size, must be a power of 2
	private static final int BUF_SIZE = 0x1000;
	
	private final ByteBuffer fileBuffer = ByteBuffer.allocate(BUF_SIZE);
	private long filePos;
	private long bufStart;
	private int bufSize = -1; 

	private long nextFilePos;  

	
	private final MapProcessor processor;
	
	// buffer for byte -> String conversions
	private final byte[] cnvBuffer; 
	
	// the o5m string table
	private String[][] stringTable;
	private final String[] stringPair;
	private int currStringTablePos;
	// a counter that must be maintained by all routines that read data

	// performance: save byte position of first occurrence of a data set type (node, way, relation)
	// to allow skipping large parts of the stream
	private long[] firstPosInFile;
	private long[] skipArray;
	
	// for delta calculations
	private long lastNodeId;
	private long lastWayId;
	private long lastRelId;
	private long[] lastRef;
	private long lastTs;
	private long lastChangeSet;
	private int lastLon, lastLat;
	
	/**
	 * A parser for the o5m format.
	 * @param processor A mapProcessor instance
	 * @param fc the file channel for the input file 
	 * @param skipArray An Array of longs that is used to hold information of file position of the first occurrence of 
	 * each known 05m data type (esp. nodes, ways, and relations). 
	 * @throws IOException 
	 */
	public O5mMapParser(MapProcessor processor, FileChannel fc, long[] skipArray) throws IOException{
		this.fileChannel = fc;
		this.processor = processor;
		this.skipArray = skipArray;
		this.skipTags = processor.skipTags();
		this.skipNodes = processor.skipNodes();
		this.skipWays = processor.skipWays();
		this.skipRels = processor.skipRels();
		this.cnvBuffer = new byte[4000]; // OSM data should not contain string pairs with length > 512
		this.stringPair = new String[2];
		this.lastRef = new long[3];
		if (skipArray == null) {
			firstPosInFile = new long[256];
			Arrays.fill(firstPosInFile, -1);
		}
		reset();
	}

	
	/**
	 * parse the input stream.
	 * @throws IOException 
	 */
	public void parse() throws IOException {
		int start = get() & 0xff;
		if (start != RESET_FLAG) 
			throw new IOException("wrong header byte " + start);
		if (skipArray != null) {
			if (skipNodes) {
				if (skipWays)
					filePos = skipArray[REL_DATASET]; // jump to first relation
				else
					filePos = skipArray[WAY_DATASET]; // jump to first way
			}
		}
		if (filePos >= 0)
			readFile();
	}
	
	/**
	 * Read the file following the initial byte.
	 * @throws IOException
	 */
	private void readFile() throws IOException {
		boolean done = false;
		while (!done) {
			long size = 0;
			int fileType = get() & 0xff;
			if (fileType >= 0 && fileType < 0xf0) {
				if (skipArray == null) {
					// save first occurrence of a data set type
					if (firstPosInFile[fileType] == -1) {
						firstPosInFile[fileType] = Math.max(0, filePos- 1);
					}
				}
				size = readUnsignedNum64();
				nextFilePos = filePos + size;
				
				
				boolean doSkip = false;
				if (fileType == NODE_DATASET && skipNodes) doSkip = true;
				else if (fileType == WAY_DATASET && skipWays) doSkip = true;
				else if (fileType == REL_DATASET && skipRels) doSkip = true;
				switch(fileType) {
				case NODE_DATASET: 
				case WAY_DATASET: 
				case REL_DATASET: 
				case BBOX_DATASET:
				case TIMESTAMP_DATASET:
				case HEADER_DATASET:
					if (doSkip) { 
						filePos = nextFilePos;
						continue;
					}
					break;					
				default:	
				}
			}
			if (fileType == NODE_DATASET) readNode();
			else if (fileType == WAY_DATASET) readWay();
			else if (fileType == REL_DATASET) readRel();
			else if (fileType == BBOX_DATASET) readBBox();
			else if (fileType == TIMESTAMP_DATASET) readFileTimestamp();
			else if (fileType == HEADER_DATASET) readHeader();
			else if (fileType == EOD_FLAG) done = true;
			else if (fileType == RESET_FLAG) reset();
			else {
				if (fileType < 0xf0)
					filePos = nextFilePos; // skip unknown data set
			}
		}
	}
	
	/**
	 * read (and ignore) the file timestamp data set.
	 * @throws IOException 
	 */
	private void readFileTimestamp() throws IOException {
		/*long fileTimeStamp = */readSignedNum64();
	}
	
	/**
	 * read the bounding box data set.
	 * @throws IOException
	 */
	private void readBBox() throws IOException {
		double leftf = 100L * readSignedNum32() * FACTOR;
		double bottomf = 100L * readSignedNum32() * FACTOR;
		double rightf = 100L * readSignedNum32() * FACTOR;
		double topf = 100L * readSignedNum32() * FACTOR;
		assert filePos == nextFilePos;
		System.out.println("Bounding box " + leftf + " " + bottomf + " " + rightf + " " + topf);

		Area area = new Area(
				Utils.toMapUnit(bottomf),
				Utils.toMapUnit(leftf),
				Utils.toMapUnit(topf),
				Utils.toMapUnit(rightf));
		if (!area.verify())
			throw new IllegalArgumentException("invalid bbox area in o5m file: " + area);

		processor.boundTag(area);
	}

	/**
	 * read a node data set.
	 * @throws IOException
	 */
	private void readNode() throws IOException{
		Node node = new Node();
		
		lastNodeId += readSignedNum64();
		if (filePos == nextFilePos)
			return; // only nodeId: this is a delete action, we ignore it 
		int version = readVersionTsAuthor();
		node.setVersion(version);
		if (filePos == nextFilePos)
			return; // only nodeId+version: this is a delete action, we ignore it 
		int lon = readSignedNum32() + lastLon; lastLon = lon;
		int lat = readSignedNum32() + lastLat; lastLat = lat;
			
		double flon = 100L * lon * FACTOR;
		double flat = 100L * lat * FACTOR;
		assert flat >= -90.0 && flat <= 90.0;
		assert flon >= -180.0 && flon <= 180.0;

		node.set(lastNodeId, flat, flon);
		readTags(node);
		elemCounter.countNode(lastNodeId);
		processor.processNode(node);
	}
	
	/**
	 * read a way data set.
	 * @throws IOException
	 */
	private void readWay() throws IOException{
		lastWayId += readSignedNum64();
		if (filePos == nextFilePos)
			return; // only wayId: this is a delete action, we ignore it 

		int version = readVersionTsAuthor();
		if (filePos == nextFilePos)
			return; // only wayId + version: this is a delete action, we ignore it 
		Way way = new Way();
		way.setId(lastWayId);
		way.setVersion(version);
		long refSize = readUnsignedNum32();
		long stop = filePos + refSize;
		
		while (filePos < stop) {
			lastRef[0] += readSignedNum64();
			way.addRef(lastRef[0]);
		}
		
		readTags(way);
		elemCounter.countWay(lastWayId);
		processor.processWay(way);
		
	}
	
	/**
	 * read a relation data set.
	 * @throws IOException
	 */
	private void readRel() throws IOException{
		lastRelId += readSignedNum64(); 
		if (filePos == nextFilePos)
			return; // only relId: this is a delete action, we ignore it 
		int version = readVersionTsAuthor();
		if (filePos == nextFilePos)
			return; // only relId + version: this is a delete action, we ignore it 
		
		Relation rel = new Relation();
		rel.setId(lastRelId);
		rel.setVersion(version);
		long refSize = readUnsignedNum32();
		long stop = filePos + refSize;
		while (filePos < stop) {
			long deltaRef = readSignedNum64();
			int refType = readRelRef();
			lastRef[refType] += deltaRef;
			rel.addMember(stringPair[0], lastRef[refType], stringPair[1]);
		}
		
		// tags
		readTags(rel);
		elemCounter.countRelation(lastRelId);
		processor.processRelation(rel);
	}
	
	private void readTags(Element elem) throws IOException{
		// we cannot skip the tags if we read relations (roles) 
		if (skipTags && skipRels) { 
			filePos = nextFilePos;
			return;
		}
		while (filePos < nextFilePos) {
			readStringPair();
			if (!skipTags) {
				elem.addTag(stringPair[0], stringPair[1]);
			}
		}
		assert filePos == nextFilePos;
		
	}
	/**
	 * Store a new string pair (length check must be performed by caller).
	 */
	private void storeStringPair() {
		stringTable[0][currStringTablePos] = stringPair[0];
		stringTable[1][currStringTablePos] = stringPair[1];
		++currStringTablePos;
		if (currStringTablePos >= STRING_TABLE_SIZE)
			currStringTablePos = 0;
	}

	/**
	 * set stringPair to the values referenced by given string reference
	 * No checking is performed.
	 * @param ref valid values are 1 .. STRING_TABLE_SIZE
	 * @throws IOException 
	 */
	private void setStringRefPair(int ref) throws IOException{
		int pos = currStringTablePos - ref;
		if (pos < 0) 
			pos += STRING_TABLE_SIZE;
		if (pos < 0 || pos >= STRING_TABLE_SIZE)
			throw new IOException("invalid string table reference: " + ref); 
		stringPair[0] = stringTable[0][pos];
		stringPair[1] = stringTable[1][pos];
	}

	/**
	 * Read version, time stamp and change set and author.  
	 * @return the version
	 * @throws IOException
	 */
	private int readVersionTsAuthor() throws IOException {
		int version = readUnsignedNum32(); 
		if (version != 0) {
			// version info
			long ts = readSignedNum64() + lastTs; lastTs = ts;
			if (ts != 0) {
				long changeSet = readSignedNum32() + lastChangeSet; lastChangeSet = changeSet;
				readAuthor();
			}
		}
		return version;
	}
	/**
	 * Read author . 
	 * @throws IOException
	 */
	private void readAuthor() throws IOException{
		int stringRef = readUnsignedNum32();
		if (stringRef == 0) {
			long toReadStart = filePos;
			long uidNum = readUnsignedNum64();
			if (uidNum == 0)
				stringPair[0] = "";
			else {
				stringPair[0] = Long.toString(uidNum);
				get(); // skip terminating zero from uid
			}
			stringPair[1] = readString();
			if (filePos - toReadStart <= MAX_STRING_PAIR_SIZE)
				storeStringPair();
		} else { 
			setStringRefPair(stringRef);
		}
		
		//System.out.println(pair[0]+ "/" + pair[1]);
	}
	
	/**
	 * read object type ("0".."2") concatenated with role (single string).
	 * @return 0..3 for type (3 means unknown)
	 */
	private int readRelRef() throws IOException {
		int refType = -1;
		long toReadStart = filePos;
		int stringRef = readUnsignedNum32();
		if (stringRef == 0) {
			refType = get() - '0';

			if (refType < 0 || refType > 2)
				refType = 3;
			stringPair[0] = REL_REF_TYPES[refType];
			stringPair[1] = readString();
			if (filePos - toReadStart <= MAX_STRING_PAIR_SIZE)
				storeStringPair();
		} else {
			setStringRefPair(stringRef);
			char c = stringPair[0].charAt(0);
			switch (c) {
			case 'n': refType = 0; break;
			case 'w': refType = 1; break;
			case 'r': refType = 2; break;
			default: refType = 3;
			}
		}
		return refType;
	}
	
	/**
	 * read a string pair (see o5m definition).
	 * @throws IOException
	 */
	private void readStringPair() throws IOException{
		int stringRef = readUnsignedNum32();
		if (stringRef == 0) {
			long toReadStart = filePos;
			int cnt = 0;
			while (cnt < 2) {
				stringPair[cnt++] = readString();
			}
			if (filePos - toReadStart <= MAX_STRING_PAIR_SIZE)
				storeStringPair();
		} else { 
			setStringRefPair(stringRef);
		}
	}
	
	/**
	 * Read a zero-terminated string (see o5m definition).
	 * @throws IOException
	 */
	String readString() throws IOException {
		int length = 0; 
		while (true) {
			final int b = get();
			if (b == 0)
				return new String(cnvBuffer, 0, length, "UTF-8");
			cnvBuffer[length++] = (byte) b;
		}
		
	}
	/** reset the delta values and string table. */
	private void reset() {
		lastNodeId = 0;
		lastWayId = 0;
		lastRelId = 0;
		lastRef[0] = 0;
		lastRef[1] = 0;
		lastRef[2] = 0;
		lastTs = 0;
		lastChangeSet = 0;
		lastLon = 0;
		lastLat = 0;
		stringTable = new String[2][STRING_TABLE_SIZE];
		currStringTablePos = 0;
	}

	/**
	 * read and verify o5m header (known values are o5m2 and o5c2).
	 * @throws IOException
	 */
	private void readHeader() throws IOException {
		byte[] header = new byte[4];
		for (int i = 0; i < header.length; i++) {
			header[i] = get();
		}
		if (header[0] != 'o' || header[1] != '5' || (header[2] != 'c' && header[2] != 'm') || header[3] != '2') {
			throw new IOException("unsupported header");
		}
	}

	/**
	 * read a varying length signed number (see o5m definition).
	 * @return the number
	 * @throws IOException
	 */
	private int readSignedNum32() throws IOException {
		int result;
		int b = get();
		result = b;
		if ((b & 0x80) == 0) { // just one byte
			if ((b & 0x01) == 1)
				return -1 - (result >> 1);
			return result >> 1;
		}
		int sign = b & 0x01;
		result = (result & 0x7e) >> 1;
		int fac = 0x40;
		while (((b = get()) & 0x80) != 0) { // more bytes will follow
			result += fac * (b & 0x7f);
			fac <<= 7;
		}
		result += fac * b;
		if (sign == 1) // negative
			return -1 - result;
		return result;

	}

	/**
	 * read a varying length signed number (see o5m definition).
	 * @return the number
	 * @throws IOException
	 */
	private long readSignedNum64() throws IOException {
		long result;
		int b = get();
		result = b;
		if ((b & 0x80) == 0) { // just one byte
			if ((b & 0x01) == 1)
				return -1 - (result >> 1);
			return result >> 1;
		}
		int sign = b & 0x01;
		result = (result & 0x7e) >> 1;
		long fac = 0x40;
		while (((b = get()) & 0x80) != 0) { // more bytes will follow
			result += fac * (b & 0x7f);
			fac <<= 7;
		}
		result += fac * b;
		if (sign == 1) // negative
			return -1 - result;
		return result;

	}
	
	/**
	 * read a varying length unsigned number (see o5m definition).
	 * @return a long
	 * @throws IOException
	 */
	private long readUnsignedNum64() throws IOException {
		int b = get();
		long result = b;
		if ((b & 0x80) == 0) { // just one byte
			return result;
		}
		result &= 0x7f;
		long fac = 0x80;
		while (((b = get()) & 0x80) != 0) { // more bytes will follow
			result += fac * (b & 0x7f);
			fac <<= 7;
		}
		result += fac * b;
		return result;
	}

	/**
	 * read a varying length unsigned number (see o5m definition)
	 * is similar to the 64 bit version.
	 * @return an int 
	 * @throws IOException
	 */
	private int readUnsignedNum32() throws IOException {
		int b = get();
		int result = b;
		if ((b & 0x80) == 0) { // just one byte
			return result;
		}
		result &= 0x7f;
		long fac = 0x80;
		while (((b = get()) & 0x80) != 0) { // more bytes will follow
			result += fac * (b & 0x7f);
			fac <<= 7;
		}
		result += fac * b;
		return result;
	}

	public long[] getSkipArray() {
		return firstPosInFile;
	}
	
	/**
	 * Read in a single byte from the current position.
	 *
	 * @return The byte that was read.
	 * @throws IOException if buffer contains no data
	 */
	private byte get() throws IOException {
		fillBuffer();

		int pos = (int) (filePos - bufStart);
		if (pos < 0 || pos >= bufSize) {
			throw new IOException("no data in file buffer, pos="+pos);
		}
		filePos++;
		return fileBuffer.get(pos);
		
		
	}
	
	/**
	 * Check to see if the buffer contains the byte at the current position.
	 * If not then it is re-read so that it does.
	 * @throws IOException in case of I/O error
	 */
	private void fillBuffer() throws IOException {
		// If we are no longer inside the buffer, then re-read it.
		if (filePos >= bufStart + bufSize) {

			// Get channel position on a block boundary.
			bufStart = filePos & ~(BUF_SIZE - 1);
			fileChannel.position(bufStart);
			// Fill buffer
			fileBuffer.clear();
			bufSize = fileChannel.read(fileBuffer);
		}
	}
}
