/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  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.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 07-Dec-2006
 */
package uk.me.parabola.imgfmt.app.trergn;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.log.Logger;

/**
 * The map is divided into areas, depending on the zoom level.  These are
 * known as subdivisions.
 *
 * A subdivision 'belongs' to a zoom level and cannot be interpreted correctly
 * without knowing the <i>bitsPerCoord</i> of the associated zoom level.
 *
 * Subdivisions also form a tree as subdivisions are further divided at
 * lower levels.  The subdivisions need to know their child divisions
 * because this information is represented in the map.
 *
 * @author Steve Ratcliffe
 */
public class Subdivision {
	private static final Logger log = Logger.getLogger(Subdivision.class);

	private static final int MAP_POINT = 0;
	private static final int MAP_INDEXED_POINT = 1;
	private static final int MAP_LINE = 2;
	private static final int MAP_SHAPE = 3;

	private final LBLFile lblFile;
	private final RGNFile rgnFile;

	// The start pointer is set for read and write.  The end pointer is only
	// set for subdivisions that are read from a file.
	private int startRgnPointer;
	private int endRgnPointer;

	private int lastMapElement;

	// The zoom level contains the number of bits per coordinate which is
	// critical for scaling quantities by.
	private final Zoom zoomLevel;

	private boolean hasPoints;
	private boolean hasIndPoints;
	private boolean hasPolylines;
	private boolean hasPolygons;
	private boolean hasRoadRefs;

	private int numPolylines;

	// The location of the central point, not scaled AFAIK
	private final int longitude;
	private final int latitude;

	// The width and the height in map units scaled by the bits-per-coordinate
	// that applies at the map level.
	private final int width;
	private final int height;

	private int number;

	// Set if this is the last one.
	private boolean last;

	private final List<Subdivision> divisions = new ArrayList<>();

	private int extTypeAreasOffset;
	private int extTypeLinesOffset;
	private int extTypePointsOffset;
	private int extTypeAreasSize;
	private int extTypeLinesSize;
	private int extTypePointsSize;

	/**
	 * Subdivisions can not be created directly, use either the
	 * {@link #topLevelSubdivision} or {@link #createSubdivision} factory
	 * methods.
	 *
	 * @param ifiles The internal files.
	 * @param area The area this subdivision should cover.
	 * @param z The zoom level.
	 */
	private Subdivision(InternalFiles ifiles, Area area, Zoom z) {
		this.lblFile = ifiles.getLblFile();
		this.rgnFile = ifiles.getRgnFile();

		this.zoomLevel = z;

		int shift = getShift();
		int mask = getMask();

		// Calculate the center, move it right and up so that it lies on a point
		// which is divisible by 2 ^shift
		this.latitude = Utils.roundUp((area.getMinLat() + area.getMaxLat())/2, shift);
		this.longitude = Utils.roundUp((area.getMinLong() + area.getMaxLong())/2, shift);
		int w = 2 * (longitude - area.getMinLong());
		int h = 2 * (latitude - area.getMinLat());
		
		// encode the values for the img format
		w = ((w + 1)/2 + mask) >> shift;
		h = ((h + 1)/2 + mask) >> shift;
		
		if (w > 0x7fff) {
			log.warn("Subdivision width is " + w + " at " + getCenter().toOSMURL());
			w = 0x7fff;
		}
		if (w < 0) { // think can get this from empty/unbounded maps
			log.error("Subdivision neg width", w, "min=", area.getMinLong(), "mid=", longitude, "max=", area.getMaxLong(), "shift=", shift, "mask=", mask, "@", getCenter().toOSMURL());
			w = 0x7fff;
		}

		if (h > 0xffff) {
			log.warn("Subdivision height is " + h + " at " + getCenter().toOSMURL());
			h = 0xffff;
		}
		if (h < 0) { // think can get this from empty/unbounded maps
			log.error("Subdivision neg height", h, "min=", area.getMinLat(), "mid=", latitude, "max=", area.getMaxLat(),
					"shift=", shift, "mask=", mask, "@", getCenter().toOSMURL());
			h = 0xffff;
		}

		this.width = w;
		this.height = h;
	}

	private Subdivision(Zoom z, SubdivData data) {
		lblFile = null;
		rgnFile = null;
		zoomLevel = z;
		latitude = data.getLat();
		longitude = data.getLon();
		this.width = data.getWidth();
		this.height = data.getHeight();

		startRgnPointer = data.getRgnPointer();
		endRgnPointer = data.getEndRgnOffset();

		int elem = data.getFlags();
		if ((elem & 0x10) != 0)
			setHasPoints(true);
		if ((elem & 0x20) != 0)
			setHasIndPoints(true);
		if ((elem & 0x40) != 0)
			setHasPolylines(true);
		if ((elem & 0x80) != 0)
			setHasPolygons(true);
		if ((elem & 0x1) != 0) // from top bit in height 
			setHasRoadRefs(true);
	}	

	/**
	 * Create a subdivision at a given zoom level.
	 *
	 * @param ifiles The RGN and LBL ifiles.
	 * @param area The (unshifted) area that the subdivision covers.
	 * @param zoom The zoom level that this division occupies.
	 *
	 * @return A new subdivision.
	 */
	public Subdivision createSubdivision(InternalFiles ifiles,
			Area area, Zoom zoom)
	{
		Subdivision div = new Subdivision(ifiles, area, zoom);
		zoom.addSubdivision(div);
		addSubdivision(div);
		return div;
	}

	/**
	 * This should be called only once per map to create the top level
	 * subdivision.  The top level subdivision covers the whole map and it
	 * must be empty.
	 *
	 * @param ifiles The LBL and  RGN ifiles.
	 * @param area The area bounded by the map.
	 * @param zoom The zoom level which must be the highest (least detailed)
     * zoom in the map.
	 * 
	 * @return The new subdivision.
	 */
	public static Subdivision topLevelSubdivision(InternalFiles ifiles,
			Area area, Zoom zoom)
	{
		Subdivision div = new Subdivision(ifiles, area, zoom);
		zoom.addSubdivision(div);
		return div;
	}

	/**
	 * Create a subdivision that only contains the number.  This is only
	 * used when reading cities and similar such usages that do not really
	 * require the full subdivision to be present.
	 * @param number The subdivision number.
	 * @return An empty subdivision.  Any operation other than getting the
	 * subdiv number is likely to fail.
	 */
	public static Subdivision createEmptySubdivision(int number) {
		Subdivision sd = new Subdivision(null, new SubdivData(0,0,0,0,0,0,0));
		sd.setNumber(number);
		return sd;
	}

	public static Subdivision readSubdivision(Zoom zoom, SubdivData subdivData) {
		return new Subdivision(zoom, subdivData);
	}

	public Zoom getZoom() {
		return zoomLevel;
	}

	/**
	 * Get the shift value, that is the number of bits to left shift by for
	 * values that need to be saved shifted in the file.  Related to the
	 * resolution.
	 *
	 * @return The shift value.  It is 24 minus the number of bits per coord.
	 * @see #getResolution()
	 */
	public final int getShift() {
		return 24 - zoomLevel.getResolution();
	}

	/**
	 * Get the shift mask.  The bits that will be lost due to the resolution
	 * shift level.
	 *
	 * @return A bit mask with the lower <i>shift</i> bits set.
	 */
	protected int getMask() {
		return (1 << getShift()) - 1;
	}

	/**
	 * Get the resolution of this division.  Resolution goes from 1 to 24
	 * and the higher the number the more detail there is.
	 *
	 * @return The resolution.
	 */
	public final int getResolution() {
		return zoomLevel.getResolution();
	}

	/**
	 * Format this record to the file.
	 *
	 * @param file The file to write to.
	 */
	public void write(ImgFileWriter file) {
		log.debug("write subdiv", latitude, longitude);
		file.put3u(startRgnPointer);
		file.put1u(getType());
		Utils.put3sLongitude(file, longitude);
		file.put3s(latitude);
		
		assert width <= 0x7fff;
		assert height <= 0xffff;
		file.put2u(width | ((last) ? 0x8000 : 0));
		file.put2u(height);

		if (!divisions.isEmpty()) {
			file.put2u(getNextLevel());
		}
	}

	public Point createPoint(String name) {
		Point p = new Point(this);
		Label label = lblFile.newLabel(name);

		p.setLabel(label);
		return p;
	}

	public Polyline createLine(String[] labels) {
		// don't be tempted to "trim()" the name as it zaps the highway shields
		Label label = lblFile.newLabel(labels[0]);
		String nameSansGC = Label.stripGarminCodes(labels[0]);
		Polyline pl = new Polyline(this);

		pl.setLabel(label);

		if(labels[1] != null) {
			// ref may contain multiple ids separated by ";"
			int maxSetIdx = 3;
			if (labels[3] == null) {
				if (labels[2] == null) {
					maxSetIdx = 1;
				} else {
					maxSetIdx = 2;
				}
			}

			String[] refs = Arrays.copyOfRange(labels, 1, maxSetIdx+1);
			if(refs.length == 1) {
				// don't bother to add a single ref that looks the
				// same as the name (sans shield) because it doesn't
				// change the routing directions
				String tr = refs[0].trim();
				String trSansGC = Label.stripGarminCodes(tr);
				if(trSansGC.length() > 0 &&
						!trSansGC.equalsIgnoreCase(nameSansGC)) {
					pl.addRefLabel(lblFile.newLabel(tr));
				}
			}
			else if (refs.length > 1){
				// multiple refs, always add the first so that it will
				// be used in routing instructions when the name has a
				// shield prefix
				pl.addRefLabel(lblFile.newLabel(refs[0].trim()));

				// only add the remaining refs if they differ from the
				// name (sans shield)
				for(int i = 1; i < refs.length; ++i) {
					String tr = refs[i].trim();
					String trSansGC = Label.stripGarminCodes(tr);
					if(trSansGC.length() > 0 &&
							!trSansGC.equalsIgnoreCase(nameSansGC)) {
						pl.addRefLabel(lblFile.newLabel(tr));
					}
				}
			}
		}
		return pl;
	}

	public void setPolylineNumber(Polyline pl) {
		pl.setNumber(++numPolylines);
	}

	public Polygon createPolygon(String name) {
		Label label = lblFile.newLabel(name);
		Polygon pg = new Polygon(this);

		pg.setLabel(label);
		return pg;
	}

	public void setNumber(int n) {
		number = n;
	}

	public void setLast(boolean last) {
		this.last = last;
	}

	public void setStartRgnPointer(int startRgnPointer) {
		this.startRgnPointer = startRgnPointer;
	}

	public int getStartRgnPointer() {
		return startRgnPointer;
	}

	public int getEndRgnPointer() {
		return endRgnPointer;
	}

	public int getLongitude() {
		return longitude;
	}

	public int getLatitude() {
		return latitude;
	}

	public void setHasPoints(boolean hasPoints) {
		this.hasPoints = hasPoints;
	}

	public void setHasIndPoints(boolean hasIndPoints) {
		this.hasIndPoints = hasIndPoints;
	}

	public void setHasPolylines(boolean hasPolylines) {
		this.hasPolylines = hasPolylines;
	}

	public void setHasPolygons(boolean hasPolygons) {
		this.hasPolygons = hasPolygons;
	}

	public void setHasRoadRefs(boolean hasRoeadRefs) {
		this.hasRoadRefs = hasRoeadRefs;
	}

	public boolean hasPoints() {
		return hasPoints;
	}

	public boolean hasIndPoints() {
		return hasIndPoints;
	}

	public boolean hasPolylines() {
		return hasPolylines;
	}

	public boolean hasPolygons() {
		return hasPolygons;
	}

	public boolean hasRoadRefs() {
		return hasRoadRefs;
	}

	/**
	 * Needed if it exists and is not first, ie there is a points
	 * section.
	 * @return true if pointer needed
	 */
	public boolean needsIndPointPtr() {
		return hasIndPoints && hasPoints;
	}

	/**
	 * Needed if it exists and is not first, ie there is a points or
	 * indexed points section.
	 * @return true if pointer needed.
	 */
	public boolean needsPolylinePtr() {
		return hasPolylines && (hasPoints || hasIndPoints);
	}

	/**
	 * Needed if it exists and is not first, ie there is a points or
	 * indexed points or polyline section.
	 * @return true if pointer needed.
	 */
	public boolean needsPolygonPtr() {
		return hasPolygons && (hasPoints || hasIndPoints || hasPolylines);
	}

	/**
	 * As this is last in the list it is needed if it exists and there
	 * is another section.
	 * @return true if pointer needed.
	 */
	public boolean needsRoadRefPtr() {
		return hasRoadRefs && (hasPoints || hasIndPoints || hasPolylines || hasPolygons);
	}
	
	public String toString() {
		return "Sub" + zoomLevel + '(' + getCenter().toOSMURL() + ')';
	}
	/**
	 * Get a type that shows if this area has lines, points etc.
	 *
	 * @return A code showing what kinds of element are in this subdivision.
	 */
	private int getType() {
		int b = 0;
		if (hasPoints)
			b |= 0x10;
		if (hasIndPoints)
			b |= 0x20;
		if (hasPolylines)
			b |= 0x40;
		if (hasPolygons)
			b |= 0x80;

		return b;
	}
	/**
	 * Get the number of the first subdivision at the next level.
	 * @return The first subdivision at the next level.
	 */
	private int getNextLevel() {
		return divisions.get(0).getNumber();
	}

	public boolean hasNextLevel() {
		return !divisions.isEmpty();
	}

	public int getExtTypeAreasOffset() {
		return extTypeAreasOffset;
	}

	public int getExtTypeLinesOffset() {
		return extTypeLinesOffset;
	}

	public int getExtTypePointsOffset() {
		return extTypePointsOffset;
	}

	public int getExtTypeAreasSize() {
		return extTypeAreasSize;
	}

	public int getExtTypeLinesSize() {
		return extTypeLinesSize;
	}

	public int getExtTypePointsSize() {
		return extTypePointsSize;
	}

	public void startDivision() {
		rgnFile.startDivision(this);
		extTypeAreasOffset = rgnFile.getExtTypeAreasSize();
		extTypeLinesOffset = rgnFile.getExtTypeLinesSize();
		extTypePointsOffset = rgnFile.getExtTypePointsSize();
		assert extTypeAreasOffset >= 0;
		assert extTypeLinesOffset >= 0;
		assert extTypePointsOffset >= 0;

	}

	public void endDivision() {
		extTypeAreasSize = rgnFile.getExtTypeAreasSize() - extTypeAreasOffset;
		extTypeLinesSize = rgnFile.getExtTypeLinesSize() - extTypeLinesOffset;
		extTypePointsSize = rgnFile.getExtTypePointsSize() - extTypePointsOffset;
		assert extTypeAreasSize >= 0;
		assert extTypeLinesSize >= 0;
		assert extTypePointsSize >= 0;
	}

	public void writeExtTypeOffsetsRecord(ImgFileWriter file) {
		file.put4(extTypeAreasOffset);
		file.put4(extTypeLinesOffset);
		file.put4(extTypePointsOffset);
		int kinds = 0;
		if(extTypeAreasSize != 0)
			++kinds;
		if(extTypeLinesSize != 0)
			++kinds;
		if(extTypePointsSize != 0)
			++kinds;
		file.put1u(kinds);
	}

	public void writeLastExtTypeOffsetsRecord(ImgFileWriter file) {
		file.put4(rgnFile.getExtTypeAreasSize());
		file.put4(rgnFile.getExtTypeLinesSize());
		file.put4(rgnFile.getExtTypePointsSize());
		file.put1u(0);
	}

	/**
	 * Read offsets for extended type data and set sizes for predecessor sub-div.
	 * Corresponds to {@link #writeExtTypeOffsetsRecord(ImgFileWriter)} 
	 * @param reader the reader
	 * @param sdPrev the pred. sub-div or null
	 * @param size the record size
	 * @param tre7Magic the magic value from TRE7 section 
	 */
	public void readExtTypeOffsetsRecord(ImgFileReader reader, Subdivision sdPrev, int size, int tre7Magic) {
		long nextPos = reader.position() + size;
		if ((tre7Magic & 0x1) != 0) 
			extTypeAreasOffset = reader.get4();
		if ((tre7Magic & 0x2) != 0)
			extTypeLinesOffset = reader.get4();
		if ((tre7Magic & 0x4) != 0)
			extTypePointsOffset = reader.get4();
		reader.position(nextPos); 
		assert extTypeAreasOffset >= 0;
		assert extTypeLinesOffset >= 0;
		assert extTypePointsOffset >= 0;

		if (sdPrev != null){
			sdPrev.extTypeAreasSize = extTypeAreasOffset - sdPrev.extTypeAreasOffset;
			sdPrev.extTypeLinesSize = extTypeLinesOffset - sdPrev.extTypeLinesOffset;
			sdPrev.extTypePointsSize = extTypePointsOffset - sdPrev.extTypePointsOffset;
			assert sdPrev.extTypeAreasSize >= 0;
			assert sdPrev.extTypeLinesSize >= 0;
			assert sdPrev.extTypePointsSize >= 0;
		}
	}
	/**
	 * Set the sizes for the extended type data. See {@link #writeLastExtTypeOffsetsRecord(ImgFileWriter)} 
	 * @param reader the reader
	 * @param size the record size
	 * @param tre7Magic the magic value from TRE7 section 
	 */
	public void readLastExtTypeOffsetsRecord(ImgFileReader reader, int size, int tre7Magic) {
		long nextPos = reader.position() + size;
		if ((tre7Magic & 0x1) != 0) 
			extTypeAreasSize = reader.get4() - extTypeAreasOffset;
		if ((tre7Magic & 0x2) != 0) 
			extTypeLinesSize = reader.get4() - extTypeLinesOffset;
		if ((tre7Magic & 0x4) != 0) 
			extTypePointsSize = reader.get4() - extTypePointsOffset;
		assert extTypeAreasSize >= 0;
		assert extTypeLinesSize >= 0;
		assert extTypePointsSize >= 0;
		reader.position(nextPos);
	}
	
	/**
	 * Add this subdivision as our child at the next level.  Each subdivision
	 * can be further divided into smaller divisions.  They form a tree like
	 * arrangement.
	 *
	 * @param sd One of our subdivisions.
	 */
	private void addSubdivision(Subdivision sd) {
		divisions.add(sd);
	}

	public int getNumber() {
		return number;
	}

	/**
	 * We are starting to draw the points.  These must be done first.
	 */
	public void startPoints() {
		if (lastMapElement > MAP_POINT)
			throw new IllegalStateException("Points must be drawn first");

		lastMapElement = MAP_POINT;
	}

	/**
	 * We are starting to draw the lines.  These must be done before
	 * polygons.
	 */
	public void startIndPoints() {
		if (lastMapElement > MAP_INDEXED_POINT)
			throw new IllegalStateException("Indexed points must be done before lines and polygons");

		lastMapElement = MAP_INDEXED_POINT;

		rgnFile.setIndPointPtr();
	}

	/**
	 * We are starting to draw the lines.  These must be done before
	 * polygons.
	 */
	public void startLines() {
		if (lastMapElement > MAP_LINE)
			throw new IllegalStateException("Lines must be done before polygons");

		lastMapElement = MAP_LINE;

		rgnFile.setPolylinePtr();
	}

	/**
	 * We are starting to draw the shapes.  This is done last.
	 */
	public void startShapes() {

		lastMapElement = MAP_SHAPE;

		rgnFile.setPolygonPtr();
	}

	/**
	 * Convert an absolute Lat to a local, shifted value
	 */
	public int roundLatToLocalShifted(int absval) {
		int shift = getShift();
		int val = absval - getLatitude();
		val += ((1 << shift) / 2);
		return (val >> shift);
	}

	/**
	 * Convert an absolute Lon to a local, shifted value
	 */
	public int roundLonToLocalShifted(int absval) {
		int shift = getShift();
		int val = absval - getLongitude();
		val += ((1 << shift) / 2);
		return (val >> shift);
	}


	public Coord getCenter(){
		return new Coord(getLatitude(),getLongitude());
	}

	/**
	 * Get the unshifted width of the subdivision.
	 * @return The true (unshifted) width.
	 */
	public int getWidth() {
		return width << getShift();
	}

	/**
	 * Get the unshifted height of the subdivision.
	 * @return The true (unshifted) height.
	 */
	public int getHeight() {
		return height << getShift();
	}
}
