/*
 * Copyright (C) 2009.
 *
 * 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.imgfmt.app.trergn;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BitReader;
import uk.me.parabola.imgfmt.app.BufferedImgFileReader;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.CoordNode;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.ImgReader;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFileReader;
import uk.me.parabola.imgfmt.app.lbl.POIRecord;
import uk.me.parabola.imgfmt.app.net.NETFileReader;
import uk.me.parabola.imgfmt.app.net.RoadDef;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.EnhancedProperties;

/**
 * The region file.  Holds actual details of points and lines etc.
 *
 * This is the view of the file when it is being read.  Use {@link RGNFile}
 * for writing the file.
 *
 * The main focus of mkgmap is creating files, there are plenty of applications
 * that read and display the data, reading is implemented only to the
 * extent required to support creating the various auxiliary files etc.
 * 
 * @author Steve Ratcliffe
 */
public class RGNFileReader extends ImgReader {
	private static final Logger log = Logger.getLogger(RGNFileReader.class);

	private final RGNHeader rgnHeader;
	private LBLFileReader lblFile;
	private NETFileReader netFile;
	private static final int FLAG_DIR = 0x40;
	
	public RGNFileReader(ImgChannel chan) {
		rgnHeader = new RGNHeader();
		setHeader(rgnHeader);

		setReader(new BufferedImgFileReader(chan));
		rgnHeader.readHeader(getReader());
	}

	public void config(EnhancedProperties props) {
		//config = props;
	}

	/**
	 * Get a list of all points for the given subdivision.  This includes
	 * both the indexed points section and the points section.
	 *
	 * The numbering of the points carries through the sections.
	 * @param sd The subdivision that we are interested in.
	 * @return A list of all points for the subdiv.
	 */
	public List<Point> pointsForSubdiv(Subdivision sd, boolean withExtType) {
		ArrayList<Point> list = new ArrayList<>();
		if (sd.hasIndPoints() || sd.hasPoints()){

			RgnOffsets rgnOffsets = getOffsets(sd);

			// Even though the indexed points are after the points, the numbering
			// starts with 1 for the first indexed point and carries on into the
			// points section.
			fetchPointsCommon(sd, rgnOffsets.getIndPointStart(), rgnOffsets.getIndPointEnd(), list);
			fetchPointsCommon(sd, rgnOffsets.getPointStart(), rgnOffsets.getPointEnd(), list);
		}
		if (withExtType && sd.getExtTypePointsSize() > 0)
			fetchPointsExtType(sd, list);

		return list;
	}

	/**
	 * The indexed points and the points sections are both read just the same.
	 */
	private void fetchPointsCommon(Subdivision sd, long start, long end, List<Point> points) {
		position(start);
		ImgFileReader reader = getReader();

		int number = points.size() + 1;
		while (position() < end) {
			Point p = new Point(sd);

			int t = reader.get1u();
			int val = reader.get3u();
			boolean hasSubtype = (val & 0x800000) != 0;
			boolean hasPoi = (val & 0x400000) != 0;

			Label l;
			int labelOffset = val & 0x3fffff;
			if (hasPoi) {
				POIRecord record = lblFile.fetchPoi(labelOffset);
				if (record != null) {
					l = record.getNameLabel();
					p.setPOIRecord(record);
				} else {
					l = lblFile.fetchLabel(0);
				}
			} else {
				l = lblFile.fetchLabel(labelOffset);
			}
			p.setLabel(l);

			p.setDeltaLong(reader.get2s());
			p.setDeltaLat(reader.get2s());

			t <<= 8;
			if (hasSubtype) {
				int st = reader.get1u();
				t |= st;
			}
			p.setType(t);

			p.setNumber(number++);
			points.add(p);
		}
	}

	/**
	 * The points with extended types
	 */
	private void fetchPointsExtType(Subdivision sd, List<Point> points) {
		long start = rgnHeader.getExtTypePointsOffset() + sd.getExtTypePointsOffset();
		long end = start + sd.getExtTypePointsSize();
		position(start);
		ImgFileReader reader = getReader();

		int number = points.size() + 1;
		while (position() < end) {
			Point p = new Point(sd);

			int type = reader.get1u() << 8;
			int b = reader.get1u();
			type |= 0x10000 + (b & 0x1f);
			p.setType(type);
			p.setDeltaLong(reader.get2s());
			p.setDeltaLat(reader.get2s());
			Label l;
			if ((b & 0x20) != 0 ){
				int labelOffset = reader.get3u();
				boolean hasPoi = (labelOffset & 0x400000) != 0;
				if (hasPoi) {
					POIRecord record = lblFile.fetchPoi(labelOffset);
					if (record != null) {
						l = record.getNameLabel();
						p.setPOIRecord(record);
					} else {
						l = lblFile.fetchLabel(0);
					}
				} else {
					l = lblFile.fetchLabel(labelOffset);
				}
				p.setLabel(l);
			}

			if ((b & 0x80) != 0){
				extractExtraBytes(reader, p);
			}
			p.setNumber(number++);
			points.add(p);
		}
	}

	/**
	 * Get all the lines for a given subdivision.
	 * @param div The subdivision we want the line from.
	 * @return A list of lines.
	 */
	public List<Polyline> linesForSubdiv(Subdivision div) {
		ArrayList<Polyline> list = new ArrayList<>();
		
		if (div.hasPolylines()){
			RgnOffsets rgnOffsets = getOffsets(div);

			int start = rgnOffsets.getLineStart();
			int end = rgnOffsets.getLineEnd();

			position(start);
			while (position() < end) {
				Polyline line = new Polyline(div);
				readLineCommon(getReader(), div, line);
				list.add(line);
			}
		}
		if (div.getExtTypeLinesSize() > 0){
			int start = rgnHeader.getExtTypeLinesOffset() + div.getExtTypeLinesOffset();
			int end = start + div.getExtTypeLinesSize();
			position(start);
			while (position() < end) {
				Polyline line = new Polyline(div);
				readLineCommonExtType(getReader(), div, line);
				list.add(line);
			}
		}
		return list;
	}

	/**
	 * Get all the polygons for a given subdivision.
	 * @param witExtTypeData 
	 */
	public List<Polygon> shapesForSubdiv(Subdivision div, boolean witExtTypeData) {
		ArrayList<Polygon> list = new ArrayList<>();
		if (div.hasPolygons()){

			RgnOffsets rgnOffsets = getOffsets(div);

			int start = rgnOffsets.getPolygonStart();
			int end = rgnOffsets.getPolygonEnd();

			position(start);

			while (position() < end) {
				Polygon line = new Polygon(div);
				readLineCommon(getReader(), div, line);
				list.add(line);
				line.setNumber(list.size());
			}
		}
		if (witExtTypeData && div.getExtTypeAreasSize() > 0) {
			int start = rgnHeader.getExtTypeAreasOffset() + div.getExtTypeAreasOffset();
			int end = start + div.getExtTypeAreasSize();
			position(start);
			while (position() < end) {
				Polygon line = new Polygon(div);
				readLineCommonExtType(getReader(), div, line);
				list.add(line);
			}
		}
		return list;
	}

	/**
	 * Since polygons are pretty much like polylines in the img format the
	 * reading code can be shared.
	 *
	 * @param reader The reader for the img file.
	 * @param div The subdivision.
	 * @param line The line or shape that is to be populated.
	 */
	private void readLineCommon(ImgFileReader reader, Subdivision div, Polyline line) {
		int type = reader.get1u();
		if (line instanceof Polygon)
			line.setType(type & 0x7f);
		else {
			line.setType(type & 0x3f);
			line.setDirection((type & FLAG_DIR) != 0);
		}
		int labelOffset = reader.get3u();
		// Extra bit (for bit stream)
		boolean extra = (labelOffset & 0x400000) != 0;
		Label label;
		if ((labelOffset & 0x800000) == 0) {
			label = lblFile.fetchLabel(labelOffset & 0x7fffff);
		} else {
				int netoff = labelOffset & 0x3fffff;
				labelOffset = netFile.getLabelOffset(netoff);
				label = lblFile.fetchLabel(labelOffset);
				RoadDef roadDef = new RoadDef(0, netoff, label.getText());
				line.setRoadDef(roadDef);
		}
		line.setLabel(label);


		line.setDeltaLong(reader.get2s());
		line.setDeltaLat(reader.get2s());
		int len;
		if ((type & 0x80) == 0)
			len = reader.get1u();
		else
			len = reader.get2u();

		int base = reader.get1u();

		byte[] bitstream = reader.get(len);
		BitReader br = new BitReader(bitstream);
		// This reads the bit stream and adds all the points found
		readBitStream(br, div.getShift(), line, extra, len, base);
	}


	/**
	 * Common code to read extended type lines or polygons for a given sub division.
	 * @param reader The reader for the img file.
	 * @param div The subdivision.
	 * @param line The line or shape that is to be populated.
	 */
	private void readLineCommonExtType(ImgFileReader reader, Subdivision div, Polyline line) {
		int type = reader.get1u() << 8;
		int b1 = reader.get1u();
		boolean hasExtraBytes = (b1 & 0x80) != 0;
		boolean hasLabel = (b1 & 0x20) != 0;
		if (!(line instanceof Polygon)) {
			line.setDirection((b1 & FLAG_DIR) != 0);
		}
		type |= 0x10000 + (b1 & 0x1f);
		line.setType(type);
		line.setDeltaLong(reader.get2s());
		line.setDeltaLat(reader.get2s());
		b1 = reader.get1u();
		int len;
		// one byte or two byte length field?
		if ((b1 & 0x01) != 0){
			len = (b1 >> 1) & 0x7f;
			assert len < 0x7f;
		}
		else {
			int b2 = reader.get1u();
			len = ((b2 << 8) + b1) >> 2;
			assert len >= 0x7f;
		}
		--len; // the encoded value includes the base field 
		assert len > 0;
		int base = reader.get1u();
		byte[] bitstream = reader.get(len);
		BitReader br = new BitReader(bitstream);
	
		// This reads the bit stream and adds all the points found, 
		readBitStream(br, div.getShift(), line, false, len, base);
	
		if (hasLabel){
			int labelOffset = reader.get3u();
			Label label = lblFile.fetchLabel(labelOffset & 0x3fffff);
			line.setLabel(label);
		}
		if (hasExtraBytes){
			extractExtraBytes(reader, line);
		}
	}

	/**
	 * Extract extra bytes.
	 * @param reader
	 */
	void extractExtraBytes(ImgFileReader reader, MapObject o){
		long pos = reader.position();
		StringBuilder sb = new StringBuilder();
		ArrayList<Byte> bytes = new ArrayList<>();
		byte b1 = reader.get();
		bytes.add(b1);
		if ((b1 & 0xe0) != 0){
			// varying length, search for 0x01 as this seems to be the terminator
			do{
				b1 = reader.get();
				bytes.add(b1);
			} while (b1 != 0x01);
		} else if ((b1 & 0xa0) != 0){
			bytes.add(reader.get());
			bytes.add(reader.get());
		}else if ((b1 & 0x80) != 0){
			bytes.add(reader.get());
		}
		for (Byte b: bytes){
			sb.append(String.format("%x", b));
		}
		ExtTypeAttributes eta = new ExtTypeAttributes(Collections.singletonMap("extra-bytes", sb.toString()), "data from img pos " + pos);
		o.setExtTypeAttributes(eta);
	}
	/**
	 * Read the bit stream for a single line in the file.
	 * @param br The bit stream reader.
	 * @param shift delta values are shifted to the left
	 * @param line The line itself.
	 * @param extra True if there is an 'extra' bit in the stream. Used for nodes.
	 * @param len The length of the stream.
	 * @param base The base size of the deltas.
	 */
	private void readBitStream(BitReader br, final int shift, Polyline line, boolean extra, int len, int base) {
		int currLat = line.getLat();
		int currLon = line.getLong();

		log.debug(String.format("Start point %.5f,%.5f",
				Utils.toDegrees(currLat),
				Utils.toDegrees(currLon)));
		
		if (extra)
			line.addCoord(new CoordNode(currLat, currLon, 0/* XXX */, false, false));
		else 
			line.addCoord(new Coord(currLat, currLon));

		int xbase = 2;
		int n = base & 0xf;
		if (n <= 9)
			xbase += n;
		else
			xbase += (2 * n) - 9;

		n = (base >>> 4) & 0xf;
		int ybase = 2;
		if (n <= 9)
			ybase += n;
		else
			ybase += (2 * n) - 9;

		if (len == 0)
			return;

		boolean xneg = false;
		boolean xsame = br.get1();
		if (xsame) {
			xneg = br.get1();
		} else {
			xbase++;
		}

		boolean ysame = br.get1();
		boolean yneg = false;
		if (ysame) {
			yneg = br.get1();
		} else {
			ybase++;
		}

		if(line.hasExtendedType()) {
			br.get1();
		} 
		
		if (extra) {
			boolean firstextra = br.get1();
			log.debug("the first extra bit is", firstextra);
		}

		// All is now prepared, read the actual deltas and decode them into
		// proper lat/long coords.
		while (br.getBitPosition() <= 8* len - ((extra ? 1:0) + xbase + ybase)) {
			br.getBitPosition();

			int dx;
			if (xsame) {
				dx = br.get(xbase);
				if (xneg)
					dx = -dx;
			} else {
				dx = br.sget2(xbase);
			}

			int dy;
			if (ysame) {
				dy = br.get(ybase);
				if (yneg)
					dy = -dy;
			} else {
				dy = br.sget2(ybase);
			}

			boolean isnode = false;
			if (extra)
				isnode = br.get1();
			
			// sometimes some 0 bits at the end of the bitstream are interpreted as an additional node
			// skip it
			if (!isnode && dx == 0 && dy == 0)
				continue;
			
			currLat += dy << shift;
			currLon += dx << shift;
			Coord coord;
			if (isnode)
				coord = new CoordNode(currLat, currLon, 0/* XXX */, false, false);
			else
				coord = new Coord(currLat, currLon);

			line.addCoord(coord);
		}
		if (line instanceof Polygon) {
			// make sure that polygon is closed
			line.addCoord(line.getPoints().get(0));
		}
	}

	/**
	 * Get the offsets to the points, lines etc in RGN for the given subdiv.
	 * @param sd The subdivision is needed to work out the starting points.
	 * @return An Offsets class that allows you to obtain the offsets.
	 */
	private RgnOffsets getOffsets(Subdivision sd) {
		int off = sd.getStartRgnPointer();
		position(rgnHeader.getDataOffset() + off);

		return new RgnOffsets(sd);
	}

	public void setLblFile(LBLFileReader lblFile) {
		this.lblFile = lblFile;
	}

	public void setNetFile(NETFileReader netFile) {
		this.netFile = netFile;
	}

	/**
	 * Class to hold the start and end points of point, lines etc within
	 * the area for a given subdivision in the RGN data.
	 */
	private class RgnOffsets {
		private final int pointOffset;
		private int pointEnd;

		private int indPointOffset;
		private int indPointEnd;

		private int lineOffset;
		private int lineEnd;

		private int polygonOffset;
		private int polygonEnd;

		private final int start;
		private int headerLen;

		/**
		 * Calculate the offsets for the given subdivision.
		 * After this is called the position will be set after any pointers that
		 * exist at the beginning of the area.
		 *
		 * @param sd The subdivision.
		 */
		private RgnOffsets(Subdivision sd) {
			ImgFileReader reader = getReader();

			start = (int) position();
			
			pointOffset = 0;

			if (sd.needsIndPointPtr()) {
				indPointOffset = reader.get2u();
				headerLen += 2;
			}
			
			if (sd.needsPolylinePtr()) {
				lineOffset = reader.get2u();
				headerLen += 2;
			}

			if (sd.needsPolygonPtr()) {
				polygonOffset = reader.get2u();
				headerLen += 2;
			}


			if (sd.hasPoints()) {
				if (sd.hasIndPoints())
					pointEnd = indPointOffset;
				else if (sd.hasPolylines())
					pointEnd = lineOffset;
				else if (sd.hasPolygons())
					pointEnd = polygonOffset;
				else
					pointEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
			}
			if (sd.hasIndPoints()) {
				if (sd.hasPolylines())
					indPointEnd = lineOffset;
				else if (sd.hasPolygons())
					indPointEnd = polygonOffset;
				else
					indPointEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
			}
			if (sd.hasPolylines()) {
				if (sd.hasPolygons())
					lineEnd = polygonOffset;
				else
					lineEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
			}
			if (sd.hasPolygons()) {
				polygonEnd = sd.getEndRgnPointer() - sd.getStartRgnPointer();
			}
		}

		public String toString() {
			return String.format("rgn div offsets: %x-%x/%x-%x/%x-%x/%x-%x",
					pointOffset, pointEnd, indPointOffset, indPointEnd,
					lineOffset, lineEnd, polygonOffset, polygonEnd);
		}

		public long getPointStart() {
			return pointOffset == 0 ? start + headerLen : start + pointOffset;
		}

		public long getPointEnd() {
			return start + pointEnd;
		}

		public long getIndPointStart() {
			return indPointOffset == 0 ? start + headerLen : start + indPointOffset;
		}

		public long getIndPointEnd() {
			return start + indPointEnd;
		}

		public int getLineStart() {
			return lineOffset == 0? start + headerLen: start + lineOffset;
		}

		public int getLineEnd() {
			return start + lineEnd;
		}

		public int getPolygonStart() {
			return polygonOffset == 0? start + headerLen: start + polygonOffset;
		}

		public int getPolygonEnd() {
			return start + polygonEnd;
		}
	}
}
