/*
 * Copyright (C) 2008
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 *
 * Create date: 07-Jul-2008
 */
package uk.me.parabola.imgfmt.app.net;

import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.log.Logger;

/**
 * An arc joins two nodes within a {@link RouteCenter}.  This may be renamed
 * to a Segment.
 * The arc also references the road that it is a part of.
 *
 * There are also links between nodes in different centers.
 *
 * @author Steve Ratcliffe
 */
public class RouteArc {
	private static final Logger log = Logger.getLogger(RouteArc.class);
	
	// Flags A
	private static final int FLAG_HASNET = 0x80;
	private static final int FLAG_FORWARD = 0x40;
	private static final int MASK_DESTCLASS = 0x7;
	public static final int MASK_CURVE_LEN = 0x38;

	// Flags B
	private static final int FLAG_LAST_LINK = 0x80;
	private static final int FLAG_EXTERNAL = 0x40;

	private int offset;

	// heading / bearing: -180 <= val < +180
	private float initialHeading; // degrees (A-> B in an arc ABCD) 
	private final float directHeading; // degrees (A-> D in an arc ABCD)

	private final RoadDef roadDef;

	// The nodes that this arc comes from and goes to
	private final RouteNode source;
	private final RouteNode dest;

	// The index in Table A describing this arc.
	private int indexA;
	// The index in Table B that this arc goes via, if external.
	private int indexB;
	
	private int flagA;
	private int flagB;

	private final boolean haveCurve;
	private final int length;
	private final byte lengthRatio;
	private final int pointsHash;
	private boolean isForward;
	private final float lengthInMeter;

	private boolean isDirect;

	// this field may limit the arcs destination class
	private byte maxDestClass = -1;
	
	// pointer to the reverse arc if this is a direct arc
	private RouteArc reverseArc;

	/**
	 * Create a new arc. An arc can contain multiple points (eg. A->B->C->D->E)
	 * @param roadDef The road that this arc segment is part of.
	 * @param source The source node. (A)
	 * @param dest The destination node (E).
	 * @param initialBearing The initial heading (signed degrees) (A->B)
	 * @param directBearing The direct heading (signed degrees) (A->E)
	 * @param arcLength the length of the arc in meter (A->B->C->D->E)
	 * @param pathLength the length of the arc in meter (summed length for additional arcs)
	 * @param directLength the length of the arc in meter (A-E) 
	 * @param pointsHash
	 */
	public RouteArc(RoadDef roadDef,
					RouteNode source, RouteNode dest,
					double initialBearing, double directBearing,
					double arcLength,
					double pathLength,
					double directLength,
					int pointsHash) {
		this.isDirect = true; // unless changed
		this.roadDef = roadDef;
		this.source = source;
		this.dest = dest;
		this.initialHeading = initialBearing >= 180 ? (float)(initialBearing - 360) : (float)initialBearing;
		// bearingTo can return +180 and inversions elsewhere simply adds 180 so return to normalised (-180 <= deg < +180)
		this.directHeading  = directBearing >= 180  ? (float)(directBearing  - 360) : (float)directBearing;
		int len = NODHeader.metersToRaw(arcLength);
		if (len >= (1 << 22)) {
			log.error("Way " + roadDef.getName() + " (id " + roadDef.getId() + ") contains an arc whose length (" + len + " units) is too big to be encoded, using length",((1 << 22) - 1));
			len = (1 << 22) - 1;
		}
		this.length = len;
		this.lengthInMeter = (float) arcLength;
		this.pointsHash = pointsHash;
		// calculate length ratio between way length on road and direct distance between end points
		int ratio = 0;
		int pathEncoded = NODHeader.metersToRaw(pathLength);
		if (pathEncoded >= 2){
			int directEncoded = NODHeader.metersToRaw(directLength);
			if (pathEncoded > directEncoded){
				ratio = (int) Math.min(31, Math.round(32.0d * directLength/ pathLength));
			}
		}
		if (ratio == 0 && len >= (1 << 14))
			ratio = 0x1f; // force encoding of curve info for very long arcs
		lengthRatio = (byte)ratio;
		haveCurve = lengthRatio > 0;
	}

	public float getInitialHeading() {
		return initialHeading;
	}

	public float getDirectHeading() {
		return directHeading;
	}

	public void setInitialHeading(float ih) {
		initialHeading = ih;
	}

	public void modInitialHeading(float adjust) {
		initialHeading += adjust;
		if (initialHeading >= 180)
			initialHeading -= 360;
		else if (initialHeading < -180)
			initialHeading += 360;
	}

	public float getFinalHeading() {
		float fh = 0;
		if (lengthInMeter != 0){
			fh = getReverseArc().getInitialHeading() + 180;
			if (fh >= 180)
				fh -= 360;
		}
		return fh;
	}

	public RouteNode getSource() {
		return source;
	}

	public RouteNode getDest() {
		return dest;
	}

	public int getLength() {
		return length;
	}

	public int getPointsHash() {
		return pointsHash;
	}

	/**
	 * Provide an upper bound for the written size in bytes.
	 */
	public int boundSize() {

		int[] lendat = encodeLength();
		//TODO: take into account that initialHeading and indexA are not always written
		// 1 (flagA) + 1-2 (offset) + 1 (indexA) + 1 (initialHeading)
		int size = 5 + lendat.length;
		if(haveCurve)
			size += encodeCurve().length;
		return size;
	}

	/**
	 * Is this an arc within the RouteCenter?
	 */
	public boolean isInternal() {
		// we might check that setInternal has been called before
		return (flagB & FLAG_EXTERNAL) == 0;
	}

	public void setInternal(boolean internal) {
		if (internal)
			flagB &= ~FLAG_EXTERNAL;
		else
			flagB |= FLAG_EXTERNAL;
	}


	/**
	 * Set this arc's index into Table A.
	 */
	public void setIndexA(int indexA) {
		this.indexA = indexA;
	}

	/**
	 * Get this arc's index into Table A.
	 *
	 * Required for writing restrictions (Table C).
	 */
	public int getIndexA() {
		return indexA;
	}

	/**
	 * Set this arc's index into Table B. Applies to external arcs only.
	 */
	public void setIndexB(int indexB) {
		assert !isInternal() : "Trying to set index on internal arc.";
		this.indexB = indexB;
	}

	/**
	 * Get this arc's index into Table B.
	 *
	 * Required for writing restrictions (Table C).
	 */
	public int getIndexB() {
		return indexB; // ??? & 0xff;
	}
	 
	public int getArcDestClass(){
		return Math.min(getRoadDef().getRoadClass(), dest.getGroup());
	}
	
	public float getLengthInMeter(){
		return lengthInMeter;
	}
	
	public static byte directionFromDegrees(float dir) {
		return (byte) Math.round(dir * 256 / 360);
	}

	/*
	 * return heading as value 0..15, where 0 is North +-11.25 degrees
	 */
	public static int compactDirFromDegrees(float dir) { 
		return ((directionFromDegrees(dir) + 8) >> 4) & 0x0f;
	}

	/**
	 * RouteArc binary format has various space saving measures:
	 * 1/ flag on first arc to indicate compactDirs format is used for all arcs in the RouteNode.
	 * 2/ flag on subsequent arcs to indicate a change in indexA==road. Uses the same bit as above.
	 * 3/ flag isForward - the arc destination is further along the line.
	 *
	 * indexA is written for the first arc and when indexA changes.
	 * initialHeading is 'required' for a new indexA or a change in isForward.
	 *   NB: addArcsToMajorRoads() adds arcs with the same indexA, initialHeading and isForward. 
	 * compactDirs uses 4 bits for the initialHeading rather than 8, giving 16 possible headings
	 *   rather than 256. These are grouped in pairs, with the other 4 bits are being used for the
	 *   next 'required' initialHeading, independent of changes to indexA/isForward.
	 *   There doesn't seem to be any detectable behaviour change in using a compactDirs pair for the
	 *   forward and reverse heading for the same road.
	 */
	public void write(ImgFileWriter writer, RouteArc lastArc, boolean useCompactDirs, Byte compactedDir) {
		boolean first = lastArc == null;
		if (first){
			if (useCompactDirs)
				flagA |= FLAG_HASNET; // first arc: the 1 tells us that initial directions are in compacted format
		} else {
			if (lastArc.getRoadDef() != this.getRoadDef())
				flagA |= FLAG_HASNET; // not first arc: the 1 tells us that we have to read table A 
			
		}

		if (isForward)
			flagA |= FLAG_FORWARD;
		
		offset = writer.position();
		if(log.isDebugEnabled())
			log.debug("writing arc at", offset, ", flagA=", Integer.toHexString(flagA));

		// fetch destination class -- will have been set correctly by now
		setDestinationClass(getArcDestClass());

		// determine how to write length and curve bit
		int[] lendat = encodeLength();

		writer.put1u(flagA);
		if (isInternal()) {
			// space for 14 bit node offset, written in writeSecond.
			writer.put1u(flagB);
			writer.put1u(0);
		} else {
			if(indexB < 0 || indexB >= 0x3f) {
				writer.put1u(flagB | 0x3f);
				writer.put1u(indexB);
			}
			else
				writer.put1u(flagB | indexB);
		}
		
		 // only write out the local net index if it is the first arc or else if newDir is set.
		if (first || lastArc.indexA != this.indexA)
			writer.put1u(indexA);

		if(log.isDebugEnabled())
			log.debug("writing length", length);
		for (int aLendat : lendat)
			writer.put((byte) aLendat);

		if (first || lastArc.indexA != this.indexA || lastArc.isForward != isForward){
			if (useCompactDirs){
				// determine if we have to write direction info
				if (compactedDir != null)
					writer.put(compactedDir); // composite byte = dont check
			} else {
				writer.put(directionFromDegrees(initialHeading));
			}
		}
		if (haveCurve) {
			int[] curvedat = encodeCurve();
			for (int aCurvedat : curvedat)
			    writer.put((byte) aCurvedat);
		}
	}

	/**
	 * Second pass over the nodes in this RouteCenter.
	 * Node offsets are now all known, so we can write the pointers
	 * for internal arcs.
	 */
	public void writeSecond(ImgFileWriter writer) {
		if (!isInternal())
			return;

		writer.position(offset + 1L);
		int val = flagB << 8;
		int diff = dest.getOffsetNod1() - source.getOffsetNod1();
		assert diff < 0x2000 && diff >= -0x2000
			: "relative pointer too large for 14 bits (source offset = " + source.getOffsetNod1() + ", dest offset = " + dest.getOffsetNod1() + ")";
		val |= diff & 0x3fff;

		// We write this big endian
		if(log.isDebugEnabled())
			log.debug("val is", Integer.toHexString(val));
		writer.put1u(val >> 8);
		writer.put1u(val & 0xff);
	}

	/*
	 * length and curve flag are stored in a variety of ways, involving
	 * 1. flagA & 0x38 (3 bits)
	 * 2. 1-3 bytes following the possible Table A index
	 *
	 * There's even more different encodings supposedly.
	 */
	private int[] encodeLength() {
		int[] lendat;
		if(length < 0x300 || (length < 0x400 && !haveCurve)) {
			// 10 bit length optional curve
			// clear bits 
			flagA &= ~0x38;
			
			if(haveCurve)
				flagA |= 0x20;
			flagA |= (length >> 5) & 0x18; // top two bits of length (at least one must be zero)
			lendat = new int[1];		   // one byte of data
			lendat[0] = length;			   // bottom 8 bits of length
			assert (flagA & 0x38) != 0x38;
		} else {
			flagA |= 0x38;		 // all three bits set
			if(length >= (1 << 14)) {
				// 22 bit length with curve
				lendat = new int[3];		   // three bytes of data
				lendat[0] = 0xC0 | (length & 0x3f); // 0x80 set, 0x40 set, 6 low bits of length
				lendat[1] = (length >> 6) & 0xff; // 8 more bits of length
				lendat[2] = (length >> 14) & 0xff; // 8 more bits of length
			}
			else if(haveCurve) {
				// 15 bit length with curve
				lendat = new int[2];		 // two bytes of data
				lendat[0] = (length & 0x7f); // 0x80 not set, 7 low bits of length
				lendat[1] = (length >> 7) & 0xff; // 8 more bits of length
			}
			else {
				// 14 bit length no curve
				lendat = new int[2]; // two bytes of data
				lendat[0] = 0x80 | (length & 0x3f); // 0x80 set, 0x40 not set, 6 low bits of length
				lendat[1] = (length >> 6) & 0xff; // 8 more bits of length
			}
		}
		return lendat;
	}

	/**
	 * Encode the curve data into a sequence of bytes.
	 * Curve data contains a ratio between arc length and direct distance
	 * and the direct bearing. This is typically encode in one byte, 
	 * for extreme ratios two bytes are used.
	 */
	private int[] encodeCurve() {
		assert lengthRatio != 0;
		int[] curveData;
		int dh = directionFromDegrees(directHeading);
		if (lengthRatio >= 1 && lengthRatio <= 17) {
			// two byte curve data neeeded
			curveData = new int[2];
			curveData[0] = lengthRatio;
			curveData[1] = dh;
			
		} else {
			// use compacted form
			int compactedRatio = lengthRatio / 2 - 8;
			assert compactedRatio > 0 && compactedRatio < 8;
			curveData = new int[1];
			curveData[0] = (compactedRatio << 5) | ((dh >> 3) & 0x1f);
			
			/* check math:
			int dhx = curveData[0] & 0x1f;
			int decodedDirectHeading = (dhx <16) ?  dhx << 3 : -(256 - (dhx<<3));
			if ((byte) (dh & 0xfffffff8) != (byte) decodedDirectHeading)
				log.error("failed to encode direct heading", directHeading, dh, decodedDirectHeading);
			int ratio = (curveData[0] & 0xe0) >> 5;
			if (ratio != compactedRatio)
				log.error("failed to encode length ratio", lengthRatio, compactedRatio, ratio);
			 */
		}
		return curveData;
	}

	public RoadDef getRoadDef() {
		return roadDef;
	}

	public void setForward() {
		isForward = true;
	}

	public boolean isForward() {
		return isForward;
	}

	public void setLast() {
		flagB |= FLAG_LAST_LINK;
	}

	protected void setDestinationClass(int destinationClass) {
		if(log.isDebugEnabled())
			log.debug("setting destination class", destinationClass);
		flagA |= (destinationClass & MASK_DESTCLASS);
	}

	public void setIndirect() {
		this.isDirect = false;
		
	}

	public boolean isDirect() {
		return isDirect;
	}

	public void setMaxDestClass(int destClass) {
		if (this.maxDestClass < 0)
			this.maxDestClass = (byte) destClass;
		else if (destClass < maxDestClass)
			maxDestClass = (byte)destClass;
	}

	@Override
	public String toString() {
		return "RouteArc [" + (isForward ? "->":"<-") + (isDirect ? "direct":"indirect")
				+ roadDef +  " " + dest + "]";
	}

	public RouteArc getReverseArc() {
		return reverseArc;
	}

	public void setReverseArc(RouteArc reverseArc) {
		this.reverseArc = reverseArc;
	}
}
