/*
 * Copyright (C) 2013, Gerd Petermann
 *
 * 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.splitter.solver;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.xmlpull.v1.XmlPullParserException;

import crosby.binary.file.BlockInputStream;
import uk.me.parabola.splitter.Area;
import uk.me.parabola.splitter.SplitFailedException;
import uk.me.parabola.splitter.Utils;
import uk.me.parabola.splitter.parser.BinaryMapParser;
import uk.me.parabola.splitter.parser.OSMXMLParser;

/**
 * Reader for precompiled sea data. This is mostly a copy of the corresponding
 * code in mkgmap SeaGenerator.
 * 
 * @author GerdP
 *
 */
public class PrecompSeaReader {

	/** The size (lat and long) of the precompiled sea tiles */
	private static final int PRECOMP_RASTER = 1 << 15;

	private static final byte SEA_TILE = 's';
	private static final byte LAND_TILE = 'l';
	private static final byte MIXED_TILE = 'm';

	// useful constants defining the min/max map units of the precompiled sea
	// tiles
	private static final int MIN_LAT = Utils.toMapUnit(-90.0);
	private static final int MAX_LAT = Utils.toMapUnit(90.0);
	private static final int MIN_LON = Utils.toMapUnit(-180.0);
	private static final int MAX_LON = Utils.toMapUnit(180.0);
	private static final Pattern keySplitter = Pattern.compile(Pattern.quote("_"));

	private final Area bounds;
	private final File precompSeaDir;
	private byte[][] precompIndex;
	private String precompSeaExt;
	private String precompSeaPrefix;
	private String precompZipFileInternalPath;
	private ZipFile zipFile;

	public PrecompSeaReader(Area bounds, File precompSeaDir) {
		this.bounds = bounds;
		this.precompSeaDir = precompSeaDir;
		init();
	}

	/**
	 * Process all precompiled sea tiles.
	 * 
	 * @param processor
	 *            The processor that is called
	 * @throws XmlPullParserException
	 */
	public void processMap(DensityMapCollector processor) throws XmlPullParserException {
		for (String tileName : getPrecompKeyNames()) {
			InputStream is = getStream(tileName);
			if (is != null) {
				try {
					if (tileName.endsWith(".pbf")) {
						BinaryMapParser binParser = new BinaryMapParser(processor, null, 0);
						BlockInputStream blockinput = (new BlockInputStream(is, binParser));
						blockinput.process();
						blockinput.close();
					} else {
						// No, try XML.
						try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
							OSMXMLParser parser = new OSMXMLParser(processor, true);
							parser.setReader(reader);
							parser.parse();
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
					throw new SplitFailedException(e.getMessage());
				}
			}
		}
	}

	/**
	 * Read the index and set corresponding fields.
	 */
	private void init() {
		if (precompSeaDir.exists()) {
			String internalPath = null;
			String indexFileName = "index.txt.gz";
			try {
				if (precompSeaDir.isDirectory()) {
					File indexFile = new File(precompSeaDir, indexFileName);
					if (!indexFile.exists()) {
						// check if the unzipped index file exists
						indexFileName = "index.txt";
						indexFile = new File(precompSeaDir, indexFileName);
					}
					if (indexFile.exists()) {
						try (InputStream indexStream = new FileInputStream(indexFile)) {
							loadIndex(indexStream, indexFileName);
						}
					} else {
						throw new IllegalArgumentException("Cannot find required index.txt[.gz] in " + precompSeaDir);
					}
				} else if (precompSeaDir.getName().endsWith(".zip")) {
					zipFile = new ZipFile(precompSeaDir);
					internalPath = "sea/";
					ZipEntry entry = zipFile.getEntry(internalPath + indexFileName);
					if (entry == null) {
						indexFileName = "index.txt";
						entry = zipFile.getEntry(internalPath + indexFileName);
					}
					if (entry == null) {
						internalPath = "";
						indexFileName = "index.txt.gz";
						entry = zipFile.getEntry(internalPath + indexFileName);
					}
					if (entry != null) {
						try (InputStream indexStream = zipFile.getInputStream(entry)) {
							precompZipFileInternalPath = internalPath;
							loadIndex(indexStream, indexFileName);
						}
					} else {
						throw new SplitFailedException("Don't know how to read " + precompSeaDir);
					}
				} else {
					throw new SplitFailedException("Don't know how to read " + precompSeaDir);
				}
			} catch (IOException exp) {
				exp.printStackTrace();
				throw new SplitFailedException("Cannot read index file " + indexFileName);
			}
		} else {
			throw new SplitFailedException(
					"Directory or zip file with precompiled sea does not exist: " + precompSeaDir.getName());
		}
	}

	private void loadIndex(InputStream indexStream, String indexFileName) throws IOException {
		if (indexFileName.endsWith(".gz")) {
			try (InputStream stream = new GZIPInputStream(indexStream)) {
				loadIndexFromStream(stream);
				return;
			}
		}
		loadIndexFromStream(indexStream);
	}

	/**
	 * Read the index from stream and populate the index grid.
	 * 
	 * @param fileStream
	 *            already opened stream
	 */
	private void loadIndexFromStream(InputStream fileStream) throws IOException {
		int indexWidth = (PrecompSeaReader.getPrecompTileStart(MAX_LON) - PrecompSeaReader.getPrecompTileStart(MIN_LON))
				/ PrecompSeaReader.PRECOMP_RASTER;
		int indexHeight = (PrecompSeaReader.getPrecompTileStart(MAX_LAT)
				- PrecompSeaReader.getPrecompTileStart(MIN_LAT)) / PrecompSeaReader.PRECOMP_RASTER;
		LineNumberReader indexReader = new LineNumberReader(new InputStreamReader(fileStream));
		Pattern csvSplitter = Pattern.compile(Pattern.quote(";"));
		String indexLine = null;

		byte[][] indexGrid = new byte[indexWidth + 1][indexHeight + 1];
		boolean detectExt = true;
		String prefix = null;
		String ext = null;

		while ((indexLine = indexReader.readLine()) != null) {
			if (indexLine.startsWith("#")) {
				// comment
				continue;
			}
			String[] items = csvSplitter.split(indexLine);
			if (items.length != 2) {
				throw new IllegalArgumentException("Invalid format in index file name: " + indexLine);
			}
			String precompKey = items[0];
			byte type = updatePrecompSeaTileIndex(precompKey, items[1], indexGrid);
			if (type == '?') {
				throw new IllegalArgumentException("Invalid format in index file name: " + indexLine);
			}
			if (type == MIXED_TILE) {
				// make sure that all file names are using the same name scheme
				int prePos = items[1].indexOf(items[0]);
				if (prePos >= 0) {
					if (detectExt) {
						prefix = items[1].substring(0, prePos);
						ext = items[1].substring(prePos + items[0].length());
						detectExt = false;
					} else {
						StringBuilder sb = new StringBuilder(prefix);
						sb.append(precompKey);
						sb.append(ext);
						if (!items[1].equals(sb.toString())) {
							throw new IllegalArgumentException("Unexpected file name in index file: " + indexLine);
						}
					}
				}
			}

		}
		//
		precompIndex = indexGrid;
		precompSeaPrefix = prefix;
		precompSeaExt = ext;
	}

	private InputStream getStream(String tileName) {
		InputStream is = null;
		try {
			if (zipFile != null) {
				ZipEntry entry = zipFile.getEntry(precompZipFileInternalPath + tileName);
				if (entry != null) {
					is = zipFile.getInputStream(entry);
				} else {
					throw new IOException("Preompiled sea tile " + tileName + " not found.");
				}
			} else {
				File precompTile = new File(precompSeaDir, tileName);
				is = new FileInputStream(precompTile);
			}
		} catch (Exception exp) {
			exp.printStackTrace();
			throw new SplitFailedException(exp.getMessage());
		}
		return is;
	}

	/**
	 * Retrieves the start value of the precompiled tile.
	 * 
	 * @param value
	 *            the value for which the start value is calculated
	 * @return the tile start value
	 */
	private static int getPrecompTileStart(int value) {
		int rem = value % PRECOMP_RASTER;
		if (rem == 0) {
			return value;
		} else if (value >= 0) {
			return value - rem;
		} else {
			return value - PRECOMP_RASTER - rem;
		}
	}

	/**
	 * Retrieves the end value of the precompiled tile.
	 * 
	 * @param value
	 *            the value for which the end value is calculated
	 * @return the tile end value
	 */
	private static int getPrecompTileEnd(int value) {
		int rem = value % PRECOMP_RASTER;
		if (rem == 0) {
			return value;
		} else if (value >= 0) {
			return value + PRECOMP_RASTER - rem;
		} else {
			return value - rem;
		}
	}

	/**
	 * Calculates the key names of the precompiled sea tiles for the bounding
	 * box. The key names are compiled of {@code lat+"_"+lon}.
	 * 
	 * @return the key names for the bounding box
	 */
	private List<String> getPrecompKeyNames() {
		List<String> precompKeys = new ArrayList<>();
		for (int lat = getPrecompTileStart(bounds.getMinLat()); lat < getPrecompTileEnd(
				bounds.getMaxLat()); lat += PRECOMP_RASTER) {
			for (int lon = getPrecompTileStart(bounds.getMinLong()); lon < getPrecompTileEnd(
					bounds.getMaxLong()); lon += PRECOMP_RASTER) {
				int latIndex = (MAX_LAT - lat) / PRECOMP_RASTER;
				int lonIndex = (MAX_LON - lon) / PRECOMP_RASTER;
				byte type = precompIndex[lonIndex][latIndex];
				if (type == MIXED_TILE)
					precompKeys.add(precompSeaPrefix + lat + "_" + lon + precompSeaExt);
			}
		}
		return precompKeys;
	}

	/**
	 * Update the index grid for the element identified by precompKey.
	 * 
	 * @param precompKey
	 *            The key name is compiled of {@code lat+"_"+lon}.
	 * @param fileName
	 *            either "land", "sea", or a file name containing OSM data
	 * @param indexGrid
	 *            the previously allocated index grid
	 * @return the byte that was saved in the index grid
	 */
	private static byte updatePrecompSeaTileIndex(String precompKey, String fileName, byte[][] indexGrid) {
		String[] tileCoords = keySplitter.split(precompKey);
		byte type = '?';
		if (tileCoords.length == 2) {
			int lat = Integer.parseInt(tileCoords[0]);
			int lon = Integer.parseInt(tileCoords[1]);
			int latIndex = (MAX_LAT - lat) / PRECOMP_RASTER;
			int lonIndex = (MAX_LON - lon) / PRECOMP_RASTER;

			if ("sea".equals(fileName))
				type = SEA_TILE;
			else if ("land".equals(fileName))
				type = LAND_TILE;
			else
				type = MIXED_TILE;

			indexGrid[lonIndex][latIndex] = type;
		}
		return type;
	}

}
