/*
 * 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.lbl;

import java.util.HashMap;
import java.util.Map;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BufferedImgFileWriter;
import uk.me.parabola.imgfmt.app.Exit;
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.BaseEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CharacterEncoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.EncodedText;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.app.trergn.Subdivision;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.log.Logger;

/**
 * The file that holds all the labels for the map.
 *
 * Would be quite simple, but there are a number of sections that hold country,
 * region, city, etc. records.
 *
 * To begin with I shall only support regular labels.
 *
 * @author Steve Ratcliffe
 */
public class LBLFile extends ImgFile {
	private static final Logger log = Logger.getLogger(LBLFile.class);

	private CharacterEncoder textEncoder = CodeFunctions.getDefaultEncoder();

	private final Map<EncodedText, Label> labelCache = new HashMap<>();

	private final LBLHeader lblHeader = new LBLHeader();

	private final PlacesFile places = new PlacesFile();
	private Sort sort;

	/** Shift value for the label offset, we always use 1 here. */
	private static final int OFFSET_MULTIPLIER = 1;

	/** Garmin software doesn't display longer labels */
	private static final int MAX_LABEL_LEN = 170;

	public LBLFile(ImgChannel chan, Sort sort) {
		this.sort = sort;
		lblHeader.setSort(sort);
		lblHeader.setOffsetMultiplier(OFFSET_MULTIPLIER);
		setHeader(lblHeader);

		setWriter(new BufferedImgFileWriter(chan, "LBL"));

		position((long) LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength());

		// The zero offset is for no label.
		getWriter().put1u(0);
		alignForNext();

		places.init(this, lblHeader.getPlaceHeader());
		places.setSort(sort);
		labelCache.put(BaseEncoder.NO_TEXT, Label.NULL_OUT_LABEL);
	}

	public void write() {
		writeBody();
	}

	public void writePost() {
		// Now that the body is written all the required offsets will be set up
		// inside the header, so we can go back and write it.
		ImgFileWriter writer = getWriter();
		getHeader().writeHeader(writer);

		// Text can be put between the header and the body of the file.
		writer.put(Utils.toBytes(sort.getDescription()));
		writer.put1u(0);
		assert writer.position() == LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength();
	}

	private void writeBody() {
		// The label section has already been written, but we need to record
		// its size before doing anything else.
		lblHeader.setLabelSize(getWriter().position() - (LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength()));
		places.write(getWriter());
	}

	public void setCharacterType(String cs, boolean forceUpper) {
		log.info("encoding type " + cs);
		CodeFunctions cfuncs = CodeFunctions.createEncoderForLBL(cs);
		
		lblHeader.setEncodingType(cfuncs.getEncodingType());
		textEncoder = cfuncs.getEncoder();
		if (forceUpper && textEncoder instanceof BaseEncoder) {
			BaseEncoder baseEncoder = (BaseEncoder) textEncoder;
			baseEncoder.setUpperCase(true);
		}
	}

	public void setEncoder(int encodingType, int codepage ) {
		CodeFunctions cfuncs = CodeFunctions.createEncoderForLBL(encodingType, codepage);
		
		lblHeader.setEncodingType(cfuncs.getEncodingType());
		textEncoder = cfuncs.getEncoder();
	}
	
	/**
	 * Add a new label with the given text.  Labels are shared, so that identical
	 * text is always represented by the same label.
	 *
	 * @param text The text of the label
	 * @return A reference to the created label
	 */
	public Label newLabel(String text) {
		return newLabel(text, true);
	}
	
	/**
	 * Add a new label with the given text.  Labels are shared, so that identical
	 * text is always represented by the same label.
	 *
	 * @param text The text of the label
	 * @param cutToMaxLen if true, cut labels to max length
	 * @return A reference to the created label
	 */
	public Label newLabel(String text, boolean cutToMaxLen) {
		if (text != null && cutToMaxLen && text.length() > MAX_LABEL_LEN) {
			int trimmedLen;
			try {
				trimmedLen = text.offsetByCodePoints(0, MAX_LABEL_LEN);
			} catch (IndexOutOfBoundsException e) {
				trimmedLen = 0; // short enough
			}
			if (trimmedLen > 0) {
				do { // logic elsewhere removes leading, trailing and multiple spaces, but now might have trailing; remove multiple anyway
					--trimmedLen;
					if (text.charAt(trimmedLen) != ' ')
						break;
				} while (trimmedLen > 0);
				text = text.substring(0, trimmedLen + 1);
			}
		}
		EncodedText encodedText = textEncoder.encodeText(text);

		Label l = labelCache.get(encodedText);
		if (l == null) {
			l = new Label(encodedText.getChars());
			labelCache.put(encodedText, l);

			l.setOffset(getNextLabelOffset());
			if (encodedText.getLength() > 0)
				getWriter().put(encodedText.getCtext(), 0, encodedText.getLength());

			alignForNext();

			if (l.getOffset() > 0x3fffff)
				throw new MapFailedException("Overflow of LBL section");
		}

		return l;
	}

	/**
	 * Align for the next label.
	 *
	 * Only has any effect when offsetMultiplier is not zero.
	 */
	private void alignForNext() {
		// Align ready for next label
		while ((getCurrentLabelOffset() & ((1 << OFFSET_MULTIPLIER) - 1)) != 0)
			getWriter().put1u(0);
	}

	private int getNextLabelOffset() {
		return getCurrentLabelOffset() >> OFFSET_MULTIPLIER;
	}

	private int getCurrentLabelOffset() {
		return position() - (LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength());
 	}

	public POIRecord createPOI(String name) {
		return places.createPOI(name);
	}

	public POIRecord createExitPOI(String name, Exit exit) {
		return places.createExitPOI(name, exit);
	}

	public void createPOIIndex(String name, int poiIndex, Subdivision group, int type) {
		places.createPOIIndex(name, poiIndex, group, type);
	}
	
	public Country createCountry(String name, String abbr) {
		return places.createCountry(name, abbr);
	}
	
	public Region createRegion(Country country, String region, String abbr) {
	    return places.createRegion(country, region, abbr);
	}
	
	public City createCity(Region region, String city, boolean unique) {
		return places.createCity(region, city, unique);
	}

	public City createCity(Country country, String city, boolean unique) {
		return places.createCity(country, city, unique);
	}

	public Zip createZip(String code) {
		return places.createZip(code);
	}

	public Highway createHighway(Region region, String name) {
		return places.createHighway(region, name);
	}

	public ExitFacility createExitFacility(int type, char direction, int facilities, String description, boolean last) {
		return places.createExitFacility(type, direction, facilities, description, last);
	}

	public void allPOIsDone() {
		places.allPOIsDone();
	}

	public void setSort(Sort sort) {
		this.sort = sort;
		lblHeader.setSort(sort);
		places.setSort(sort);
	}

	public int numCities() {
		return places.numCities();
	}

	public int numZips() {
		return places.numZips();
	}

	public int numHighways() {
		return places.numHighways();
	}

	public int numExitFacilities() {
		return places.numExitFacilities();
	}

	public int getCodePage() {
		return lblHeader.getCodePage();
	}
}
