/*
 * 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: 03-Dec-2006
 */
package uk.me.parabola.imgfmt.app.trergn;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.BufferedImgFileWriter;
import uk.me.parabola.imgfmt.app.ImgFile;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.labelenc.EncodedText;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;
import uk.me.parabola.util.Configurable;
import uk.me.parabola.util.EnhancedProperties;

/**
 * This is the file that contains the overview of the map.  There
 * can be different zoom levels and each level of zoom has an
 * associated set of subdivided areas.  Each of these areas then points
 * into the RGN file.
 *
 * This is quite a complex file as there are quite a few miscellaneous pieces
 * of information stored.
 *
 * @author Steve Ratcliffe
 */
public class TREFile extends ImgFile implements Configurable {
	private static final Logger log = Logger.getLogger(TREFile.class);

	// Zoom levels for map
	private final Zoom[] mapLevels = new Zoom[16];

	private final List<Label> copyrights = new ArrayList<>();

	// Information about polylines.  eg roads etc.
	private final List<PolylineOverview> polylineOverviews = new ArrayList<>();

	private final List<PolygonOverview> polygonOverviews = new ArrayList<>();

	private final List<PointOverview> pointOverviews = new ArrayList<>();

	private int lastRgnPos;

	private final TREHeader header = new TREHeader();


	public TREFile(ImgChannel chan) {
		setHeader(header);
		setWriter(new BufferedImgFileWriter(chan, "TRE"));

		// Position at the start of the writable area.
		position(header.getHeaderLength());
	}

	public Zoom createZoom(int zoom, int bits) {
		Zoom z = new Zoom(zoom, bits);
		mapLevels[zoom] = z;
		return z;
	}

	/**
	 * Add a string to the 'mapinfo' section.  This is a section between the
	 * header and the start of the data.  Nothing points to it directly.
	 *
	 * @param enc A string in the EncodedText format.
	 */
	public void addInfo(EncodedText enc) {
		byte[] val = enc.getCtext();
		if (position() != header.getHeaderLength() + header.getMapInfoSize())
			throw new IllegalStateException("All info must be added before anything else");

		header.setMapInfoSize(header.getMapInfoSize() + enc.getLength() + 1);
		getWriter().put(val);
		getWriter().put1u(0);
	}

	public void addCopyright(Label cr) {
		copyrights.add(cr);
	}

	public void addPointOverview(PointOverview ov) {
		pointOverviews.add(ov);
	}

	public void addPolylineOverview(PolylineOverview ov) {
		polylineOverviews.add(ov);
	}

	public void addPolygonOverview(PolygonOverview ov) {
		polygonOverviews.add(ov);
	}

	public void config(EnhancedProperties props) {
		header.config(props);
	}

	/**
	 * Write out the body of the TRE file.  The act of writing the body sections
	 * out provides us with pointers that are needed for the header.  Therefore
	 * the header needs to be written after the body (or obviously we could
	 * make two passes).
	 */
	private void writeBody(boolean includeExtendedTypeData) {
		writeMapLevels();

		writeSubdivs();

		writeCopyrights();

		writeOverviews();

		if(includeExtendedTypeData) {
			writeExtTypeOffsetsRecords();
			writeExtTypeOverviews();
		}
	}

	/**
	 * Write out the subdivisions.  This is quite complex as they have to be
	 * numbered and written out keeping their parent/child relationship
	 * intact.
	 */
	private void writeSubdivs() {
		header.setSubdivPos(position());
		int subdivnum = 1; // numbers start at one

		// First prepare to number them all
		for (int i = 15; i >= 0; i--) {
			Zoom z = mapLevels[i];
			if (z == null)
				continue;

			Iterator<Subdivision> it = z.subdivIterator();
			while (it.hasNext()) {
				Subdivision sd = it.next();
				log.debug("setting number to", subdivnum);
				sd.setNumber(subdivnum++);
			}
		}

		// Now we can write them all out.
		for (int i = 15; i >= 0; i--) {
			Zoom z = mapLevels[i];
			if (z == null)
				continue;

			Iterator<Subdivision> it = z.subdivIterator();
			while (it.hasNext()) {
				Subdivision sd = it.next();

				sd.write(getWriter());
				if (sd.hasNextLevel())
					header.setSubdivSize(header.getSubdivSize() + TREHeader.SUBDIV_REC_SIZE2);
				else
					header.setSubdivSize(header.getSubdivSize() + TREHeader.SUBDIV_REC_SIZE);
			}
		}
		getWriter().put4(lastRgnPos);
		header.setSubdivSize(header.getSubdivSize() + 4);
	}

	private void writeExtTypeOffsetsRecords() {
		header.setExtTypeOffsetsPos(position());
		Subdivision sd = null;
		for (int i = 15; i >= 0; i--) {
			Zoom z = mapLevels[i];
			if (z == null)
				continue;

			Iterator<Subdivision> it = z.subdivIterator();
			while (it.hasNext()) {
				sd = it.next();
				sd.writeExtTypeOffsetsRecord(getWriter());
				header.incExtTypeOffsetsSize();
			}
		}
		if(sd != null) {
			sd.writeLastExtTypeOffsetsRecord(getWriter());
			header.incExtTypeOffsetsSize();
		}
	}

	/**
	 * Write out the map levels.  This is a mapping between the level number
	 * and the resolution.
	 */
	private void writeMapLevels() {
		// Write out the map levels (zoom)
		header.setMapLevelPos(position());
		for (int i = 15; i >= 0; i--) {
			// They need to be written in reverse order I think
			Zoom z = mapLevels[i];
			if (z == null)
				continue;
			header.setMapLevelsSize(header.getMapLevelsSize() + TREHeader.MAP_LEVEL_REC_SIZE);
			z.write(getWriter());
		}
	}

	/**
	 * Write out the overview section.  This is a mapping between the map feature
	 * type and the highest level (lowest detail) that it appears at.  There
	 * are separate ones for points, lines and polygons.
	 */
	private void writeOverviews() {
		header.setPointPos(position());
		
		// Point overview section
		pointOverviews.sort(null);
		for (Overview ov : pointOverviews) {
			if(!ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incPointSize();
			}
		}

		// Line overview section.
		header.setPolylinePos(position());
		polylineOverviews.sort(null);
		for (Overview ov : polylineOverviews) {
			if(!ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incPolylineSize();
			}
		}

		// Polygon overview section
		header.setPolygonPos(position());
		polygonOverviews.sort(null);
		for (Overview ov : polygonOverviews) {
			if(!ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incPolygonSize();
			}
		}
	}

	private void writeExtTypeOverviews() {

		header.setExtTypeOverviewsPos(position());

		// assumes overviews are already sorted

		for (Overview ov : polylineOverviews) {
			if(ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incExtTypeOverviewsSize();
				header.incNumExtTypeLineTypes();
			}
		}

		for (Overview ov : polygonOverviews) {
			if(ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incExtTypeOverviewsSize();
				header.incNumExtTypeAreaTypes();
			}
		}

		for (Overview ov : pointOverviews) {
			if(ov.hasExtType()) {
				ov.setMaxLevel(decodeLevel(ov.getMinResolution()));
				ov.write(getWriter());
				header.incExtTypeOverviewsSize();
				header.incNumExtTypePointTypes();
			}
		}
	}

	/**
	 * Convert a min resolution to a level.  We return the lowest level (most
	 * detailed) that has a resolution less than or equal to the given resolution.
	 * 
	 * @param minResolution The minimum resolution.
	 * @return The level corresponding to the resolution.
	 */
	private int decodeLevel(int minResolution) {
		Zoom top = null;
		for (int i = 15; i >= 0; i--) {
			Zoom z = mapLevels[i];
			if (z == null)
				continue;

			if (top == null)
				top = z;

			if (z.getResolution() >= minResolution)
				return z.getLevel();
		}

		// If not found, then allow it only at the top level
		if (top != null)
			return top.getLevel();
		else
			return 0; // Fail safe, shouldn't really happen
	}

	/**
	 * Write out the copyrights.  This is just a list of pointers to strings
	 * in the label section basically.
	 */
	private void writeCopyrights() {
		// Write out the pointers to the labels that hold the copyright strings
		header.setCopyrightPos(position());
		ImgFileWriter writer = getWriter();
		for (Label l : copyrights) {
			header.incCopyrightSize();
			writer.put3u(l.getOffset());
		}
	}

	public void setLastRgnPos(int lastRgnPos) {
		TREFile.this.lastRgnPos = lastRgnPos;
	}

	public void write(boolean includeExtendedTypeData) {
		// Do anything that is in structures and that needs to be dealt with.
		writeBody(includeExtendedTypeData);
	}

	public void writePost() {
		// Now refresh the header
		position(0);
		getHeader().writeHeader(getWriter());
	}

	public void setMapId(int mapid) {
		header.setMapId(mapid);
	}

	public void setBounds(Area area) {
		header.setBounds(area);
	}

	public void addPoiDisplayFlags(int b) {
		header.addPoiDisplayFlags(b);
	}

	public void setDriveOnLeft(boolean b) {
		header.setDriveOnLeft(b);
	}
}
