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

import static uk.me.parabola.imgfmt.app.Label.NULL_LABEL;

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

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.BufferedImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFile;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.Section;
import uk.me.parabola.imgfmt.app.labelenc.CharacterDecoder;
import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions;
import uk.me.parabola.imgfmt.app.labelenc.DecodedText;
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.
 *
 * There are also a number of sections that hold country,
 * region, city, etc. records.
 *
 * 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 LBLFileReader extends ImgFile {

	private CharacterDecoder textDecoder = CodeFunctions.getDefaultDecoder();

	private final LBLHeader header = new LBLHeader();

	private final Int2ObjectOpenHashMap<Label> labels = new Int2ObjectOpenHashMap<>();
	private final Int2ObjectOpenHashMap<POIRecord> pois = new Int2ObjectOpenHashMap<>();
	private final List<Country> countries = new ArrayList<>();
	private final List<Region> regions = new ArrayList<>();
	private final Int2ObjectOpenHashMap<Zip> zips = new Int2ObjectOpenHashMap<>();
	private final List<City> cities = new ArrayList<>();

	public LBLFileReader(ImgChannel chan) {
		this(chan, true, 0);
	}
	
	public LBLFileReader(ImgChannel chan, boolean fullData, int gmpOffset) {
		setHeader(header);

		setReader(new BufferedImgFileReader(chan, gmpOffset));
		header.readHeader(getReader());
		if (!fullData)
			return;

		int offsetMultiplier = header.getOffsetMultiplier();
		CodeFunctions funcs = CodeFunctions.createEncoderForLBL(
				header.getEncodingType(), header.getCodePage());
		textDecoder = funcs.getDecoder();

		readLables(offsetMultiplier);

		readCountries();
		readRegions();

		readCities();
		readZips();
		readPoiInfo();
	}

	/**
	 * Get a label by its offset in the label area.
	 * @param offset The offset in the label section.  The offset 0 always
	 * is an empty string.
	 * @return The label including its text.
	 */
	public Label fetchLabel(int offset) {
		Label label = labels.get(offset);
		if (label == null) {
			assert offset == 0 : "Invalid label offset found " + offset;
			return NULL_LABEL;
		}

		return label;
	}

	/**
	 * Get a list of cites.  This is not cached here.
	 * @return A list of City objects.
	 */
	public List<City> getCities() {
		return cities;
	}

	public List<Country> getCountries() {
		return Collections.unmodifiableList(countries);
	}

	public List<Region> getRegions() {
		return Collections.unmodifiableList(regions);
	}
	
	public List<Zip> getZips() {
		return new ArrayList<>(zips.values());
	}

	/**
	 * Return POI information.
	 * @param offset The offset of the poi information in the header.
	 * @return Returns a poi record at the given offset.  Returns null if
	 * there isn't one at that offset (probably a bug if that does happen though...).
	 */
	public POIRecord fetchPoi(int offset) {
		return pois.get(offset);
	}

	/**
	 * Read a cache the countries. These are used when reading cities.
	 */
	private void readCountries() {
		ImgFileReader reader = getReader();

		PlacesHeader placeHeader = header.getPlaceHeader();

		countries.add(null); // 1 based indexes
		Section s = placeHeader.getCountrySection();
		int start = s.getPosition();
		int end = s.getEndPos();
		int recSize = s.getItemSize();
		int unkSize = recSize - 3;   // new maps may have extra data
		reader.position(start);
		int index = 1;
		while (reader.position() < end) {
			int offset = reader.get3u();
			Label label = fetchLabel(offset & 0x3fffff);
			if (unkSize > 0)
				reader.get(unkSize);

			if (label != null) {
				Country country = new Country(index, label);
				countries.add(country);
			}
			index++;
		}
	}

	/**
	 * Read an cache the regions.  These are used when reading cities.
	 */
	private void readRegions() {
		ImgFileReader reader = getReader();

		PlacesHeader placeHeader = header.getPlaceHeader();
		Section s = placeHeader.getRegionSection();
		int start = s.getPosition();
		int end = s.getEndPos();
		int recSize = s.getItemSize();
		int unkSize = recSize - 5;   // new maps may have extra data

		regions.add(null);

		reader.position(start);
		int index = 1;
		while (reader.position() < end) {
			int country = reader.get2u();
			int offset = reader.get3u();
			if (unkSize > 0)
				reader.get(unkSize);
			Label label = fetchLabel(offset & 0x3fffff);
			if (label != null) {
				Region region = new Region(countries.get(country), label);
				region.setIndex(index);

				regions.add(region);
			}

			index++;
		}
	}

	/**
	 * Read in the city section and cache the results here.  They are needed
	 * to read in the POI properties section.
	 */
	private void readCities() {
		PlacesHeader placeHeader = header.getPlaceHeader();
		Section s = placeHeader.getCitySection();
		int start = s.getPosition();
		int end = s.getEndPos();
		int recSize = s.getItemSize();
		int unkSize = recSize - 5;   // new maps may have extra data
		ImgFileReader reader = getReader();

		// Since cities are indexed starting from 1, we add a null one at index 0

		reader.position(start);
		int index = 1;
		while (reader.position() < end) {
			// First is either a label offset or a point/subdiv combo, we
			// don't know until we have read further
			int label = reader.get3u();
			int info = reader.get2u();
			if (unkSize > 0)
				reader.get(unkSize);

			City city;
			if ((info & 0x4000) == 0) {
				Region region = regions.get(info & 0x3fff);
				city = new City(region);
			} else {
				Country country = countries.get(info & 0x3fff);
				city = new City(country);
			}

			city.setIndex(index);
			if ((info & 0x8000) == 0) {
				city.setSubdivision(Subdivision.createEmptySubdivision(1));
				Label label1 = fetchLabel(label & 0x3fffff);
				city.setLabel(label1);
			} else {
				// Has subdiv/point index
				int pointIndex = label & 0xff;
				int subdiv = (label >> 8) & 0xffff;
				city.setPointIndex(pointIndex);
				city.setSubdivision(Subdivision.createEmptySubdivision(subdiv));
			}
			cities.add(city);

			index++;
		}
	}

	/**
	 * Read and cache all the labels.
	 *
	 * Note: It is pretty pointless saving the whole label rather than just
	 * the text, except that other objects take a Label.  Perhaps this can
	 * be changed.
	 */
	private void readLables(int mult) {
		ImgFileReader reader = getReader();

		labels.put(0, NULL_LABEL);

		int start = header.getLabelStart();
		int size =  header.getLabelSize();

		reader.position((long) start + mult);
		int labelOffset = mult;

		for (int off = mult; off <= size; off++) {
			byte b = reader.get();
			if (textDecoder.addByte(b)) {
				labelOffset = saveLabel(labelOffset, off, mult);

				// If there is an offset multiplier greater than one then padding will be used to
				// ensure that the labels are on suitable boundaries.  We must skip over any such padding.
				while ((labelOffset & (mult - 1)) != 0) {
					textDecoder.reset();
					if (labelOffset <= off) {
						// In the 6bit decoder, we may have already read the (first) padding byte and so
						// we increment the label offset without reading anything more.
						labelOffset++;
					} else {
						reader.get();
						//noinspection AssignmentToForLoopParameter
						off++;
						labelOffset++;
					}
				}
			}
		}
	}

	/**
	 * We have a label and we need to save it.
	 *
	 * @param labelOffset The offset of the label we are about to save.
	 * @param currentOffset The current offset that last read from.
	 * @param multiplier The label offset multiplier.
	 * @return The offset of the next label.
	 */
	private int saveLabel(int labelOffset, int currentOffset, int multiplier) {
		DecodedText encText = textDecoder.getText();
		String text = encText.getText();

		Label label = new Label(text);
		assert (labelOffset & (multiplier - 1)) == 0;
		int adustedOffset = labelOffset / multiplier;
		label.setOffset(adustedOffset);
		labels.put(adustedOffset, label);

		// Calculate the offset of the next label. This is not always
		// the current offset + 1 because there may be bytes left
		// inside the decoder.
		return currentOffset + 1 + encText.getOffsetAdjustment();
	}

	
	/**
	 * Reads the zips.
	 */
	private void readZips() {
		ImgFileReader reader = getReader();

		PlacesHeader placeHeader = header.getPlaceHeader();
		Section s = placeHeader.getZipSection();
		int start = s.getPosition();
		int end = s.getEndPos();
		int recSize = s.getItemSize();
		int unkSize = recSize - 3;   // new maps may have extra data

		reader.position(start);

		int zipIndex = 1;
		while (reader.position() < end) {
			int lblOffset = reader.get3u();
			if (unkSize > 0)
				reader.get(unkSize);

			Zip zip = new Zip(fetchLabel(lblOffset & 0x3fffff));
			zip.setIndex(zipIndex);
			
			zips.put(zip.getIndex(), zip);
			
			zipIndex++;
		}
	}
	
	/**
	 * Read all the POI information.
	 * This will create a POIRecord, but we just get the name at the minute.
	 *
	 * TODO: not finished
	 */
	private void readPoiInfo() {
		ImgFileReader reader = getReader();

		PlacesHeader placeHeader = header.getPlaceHeader();
		int poiGlobalFlags = placeHeader.getPOIGlobalFlags();

		int start = placeHeader.getPoiPropertiesStart();
		int end = placeHeader.getPoiPropertiesEnd();

		reader.position(start);

		PoiMasks localMask = makeLocalMask(placeHeader);

		while (reader.position() < end) {
			int poiOffset = position() - start;
			int val = reader.get3u();
			int labelOffset = val & 0x3fffff;


			boolean override = (val & 0x800000) != 0;

			POIRecord poi = new POIRecord();
			poi.setLabel(fetchLabel(labelOffset));

			// We have what we want, but now have to find the start of the
			// next record as they are not fixed length.
			int flags;
			boolean hasStreet;
			boolean hasStreetNum;
			boolean hasCity;
			boolean hasZip;
			boolean hasPhone;
			boolean hasHighwayExit;
			boolean hasTides;

			if (override) {
				flags = reader.get1u();

				hasStreetNum = (flags & localMask.streetNumMask) != 0;
				hasStreet = (flags & localMask.streetMask) != 0;
				hasCity = (flags & localMask.cityMask) != 0;
				hasZip = (flags & localMask.zipMask) != 0;
				hasPhone = (flags & localMask.phoneMask) != 0;
				hasHighwayExit = (flags & localMask.highwayExitMask) != 0;
				hasTides = (flags & localMask.tidesMask) != 0;
			} else {
				flags = poiGlobalFlags;

				hasStreetNum = (flags & POIRecord.HAS_STREET_NUM) != 0;
				hasStreet = (flags & POIRecord.HAS_STREET) != 0;
				hasCity = (flags & POIRecord.HAS_CITY) != 0;
				hasZip = (flags & POIRecord.HAS_ZIP) != 0;
				hasPhone = (flags & POIRecord.HAS_PHONE) != 0;
				hasHighwayExit = (flags & POIRecord.HAS_EXIT) != 0;
				hasTides = (flags & POIRecord.HAS_TIDE_PREDICTION) != 0;
			}

			if (hasStreetNum) {
				byte b = reader.get();
				if ((b & 0x80) == 0) {
					int mpoffset = (b << 16) & 0xff0000;
					mpoffset |= reader.get2u();

					poi.setComplexStreetNumber(fetchLabel(mpoffset));
				} else {
					poi.setSimpleStreetNumber(reader.getBase11str(b, '-'));
				}
			}

			if (hasStreet) {
				int streetNameOffset = reader.get3u();// label for street
				Label label = fetchLabel(streetNameOffset);
				poi.setStreetName(label);
			}

			if (hasCity) {
				int cityIndex = reader.getNu(Utils.numberToPointerSize(placeHeader.getNumCities()));
				poi.setCity(cities.get(cityIndex-1));
			}

			if (hasZip) {
				int zipIndex = reader.getNu(Utils.numberToPointerSize(placeHeader.getNumZips()));
				poi.setZip(zips.get(zipIndex-1));
			}
			
			if (hasPhone) {
				byte b = reader.get();
				if ((b & 0x80) == 0) {
					// Yes this is a bit strange it is a byte followed by a char
					int mpoffset = (b << 16) & 0xff0000;
					mpoffset |= reader.get2u();

					poi.setComplexPhoneNumber(fetchLabel(mpoffset));
				} else {
					poi.setSimplePhoneNumber(reader.getBase11str(b, '-'));
				}
			}

			if (hasHighwayExit) {
				int lblinfo = reader.get3u();
				int highwayLabelOffset = lblinfo & 0x3FFFF;
				boolean indexed = (lblinfo & 0x800000) != 0;
				boolean overnightParking = (lblinfo & 0x400000) != 0;

				int highwayIndex = reader.getNu(Utils.numberToPointerSize(placeHeader.getNumHighways()));
				if (indexed) {
					int eidx = reader.getNu(Utils.numberToPointerSize(placeHeader.getNumExits()));
				}
			}

			if (hasTides) {
				Logger.defaultLogger.warn("Map has tide prediction, please implement!");
			}

			pois.put(poiOffset, poi);
		}
	}

	/**
	 * The meaning of the bits in the local flags depends on which bits
	 * are set in the global flags.  Hence we have to calculate the
	 * masks to use.  These are held in an instance of PoiMasks
	 * @param placeHeader The label header.
	 * @return The masks as modified by the global flags.
	 */
	private PoiMasks makeLocalMask(PlacesHeader placeHeader) {
		int globalPoi = placeHeader.getPOIGlobalFlags();

		int mask= 0x1;

		boolean hasStreetNum = (globalPoi & POIRecord.HAS_STREET_NUM) != 0;
		boolean hasStreet = (globalPoi & POIRecord.HAS_STREET) != 0;
		boolean hasCity = (globalPoi & POIRecord.HAS_CITY) != 0;
		boolean hasZip = (globalPoi & POIRecord.HAS_ZIP) != 0;
		boolean hasPhone = (globalPoi & POIRecord.HAS_PHONE) != 0;
		boolean hasHighwayExit = (globalPoi & POIRecord.HAS_EXIT) != 0;
		boolean hasTides = (globalPoi & POIRecord.HAS_TIDE_PREDICTION) != 0;

		PoiMasks localMask = new PoiMasks();

		if (hasStreetNum) {
			localMask.streetNumMask = mask;
			mask <<= 1;
		}

		if (hasStreet) {
			localMask.streetMask = mask;
			mask <<= 1;
		}

		if (hasCity) {
			localMask.cityMask = mask;
			mask <<= 1;
		}

		if (hasZip) {
			localMask.zipMask = mask;
			mask <<= 1;
		}

		if (hasPhone) {
			localMask.phoneMask = mask;
			mask <<= 1;
		}

		if (hasHighwayExit) {
			localMask.highwayExitMask = mask;
			mask <<= 1;
		}

		if (hasTides) {
			localMask.tidesMask = mask;
			mask <<= 1;
		}

		return localMask;
	}

	public Map<Integer, String> getLabels() {
		Map<Integer, String> m = new HashMap<>();
		for (Map.Entry<Integer, Label> ent : labels.entrySet()) {
			m.put(ent.getKey(), ent.getValue().getText());
		}
		return m;
	}

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

	public int getSortOrderId() {
		return header.getSortOrderId();
	}

	private class PoiMasks {
		private int streetNumMask;
		private int streetMask;
		private int cityMask;
		private int zipMask;
		private int phoneMask;
		private int highwayExitMask;
		private int tidesMask;
	}

	public int getEncodingType() {
		return header.getEncodingType();
	}
}
