/*
 * Copyright (C) 2017.
 *
 * 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.mkgmap.reader.hgt;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.log.Logger;

import static java.nio.channels.FileChannel.MapMode.READ_ONLY;

/**
 * Rather simple code to read a single HGT file with SRTM data. Based on old code in class HGTDEM 
 * in package uk.me.parabola.mkgmap.reader.dem which was removed in 2017.
 * @author Gerd Petermann
 *
 */
public class HGTReader {
	private static final Logger log = Logger.getLogger(HGTReader.class);

	public static final short UNDEF = Short.MIN_VALUE;
	private ByteBuffer buffer;
	private int res;
	private final String fileName;
	private String path;
	private boolean read;
	private long count;
	private int numPixelsX;
	private int numPixelsY;

	
	private static final Map<String,Set<String>> missingMap = new HashMap<>();
	private static final Set<String> badDir = new HashSet<>();
	
	/**
	 * Class to read a single HGT file. 
	 * @param lat in degrees, -90 .. 90
	 * @param lon - -180..180
	 * @param dirsWithHGT string with comma separated list of directories to search for *.hgt files
	 * Supported are also zip files containing *.hgt files and directories containing *.hgt.zip.
	 */
	public HGTReader(int lat, int lon, String dirsWithHGT) {
		String baseName = String.format("%s%02d%s%03d",
				lat < 0 ? "S" : "N", lat < 0 ? -lat : lat, 
						lon < 0 ? "W" : "E", lon < 0 ? -lon : lon);
		
 		String[] dirs = dirsWithHGT.split("[,]");
		fileName = baseName + ".hgt";
		String fName = ".";
		boolean knownAsMissing = false;
		synchronized (missingMap) {
			Set<String> missingSet = missingMap.get(dirsWithHGT);
			if (missingSet != null)
				knownAsMissing = missingSet.contains(fileName);
		}
		if (!knownAsMissing) {
			for (String dir : dirs) {
				dir = dir.trim();
				File f = new File (dir);
				if (!f.exists()) {
					synchronized (badDir) {
						if (badDir.add(dir))
							log.error("extracted path >" + dir + "< does not exist, check option dem:", dirsWithHGT);
					}
					continue;
				}
				if (f.isDirectory()) {
					fName = Utils.joinPath(dir, fileName);
					try (FileInputStream fis = new FileInputStream(fName)) {
						res = calcRes(fis.getChannel().size(), fName);
						if (res >= 0) {
							path = fName;
						}
						break;
					} catch (FileNotFoundException e) {
					} catch (IOException e) {
						log.error("failed to get size for file", fName);
					}
					fName += ".zip";
					checkZip(fName, fileName); // try to find *.hgt.zip in dir that contains *.hgt
					if (res > 0) {
						path = fName;
						return;
					}
					fName = Utils.joinPath(dir, baseName) + ".zip";
					checkZip(fName, fileName); // try to find *.hgt.zip in dir that contains *.hgt
					if (res > 0) {
						path = fName;
						return;
					}
				} else {
					if (dir.endsWith(".zip")) {
						checkZip(dir, fileName); // try to find *.hgt in zip file
					}
				}
				if (res > 0) {
					path = dir;
					return;
				}
			}
			if (res <= 0 || path == null) {
				res = -1;
				path = null;
				synchronized (missingMap){
					missingMap.computeIfAbsent(dirsWithHGT, k-> new HashSet<>()).add(fileName);
				}
				HGTList hgtList = HGTList.get();
				if (hgtList != null) {
					if (hgtList.shouldExist(lat, lon))
						Logger.defaultLogger.warn(this.getClass().getSimpleName() + ": file " + fileName + " not found but it should exist. Height values will be 0.");
				} else { 
					log.warn("file " + fileName + " not found. Is expected to cover sea.");
				}
			}
		}
	}
	
	/**
	 * try to find the needed file. Different hgt providers use slightly different methods to
	 * pack their data. 
	 * @param zipFile
	 * @param name
	 * @return
	 */
	private ZipEntry findZipEntry (ZipFile zipFile, String name) {
		ZipEntry entry = zipFile.getEntry(name);
		if (entry == null) {
			// no direct hit, try to recurse through all files
			Enumeration<? extends ZipEntry> entries = zipFile.entries();
			while (entries.hasMoreElements()) {
				entry = entries.nextElement();
				if (!entry.isDirectory() && entry.getName().toUpperCase().endsWith(name.toUpperCase())) {
					return entry;
				}
			}
			return null;
		}
		return entry;
	}
	
	/**
	 * Check if we can find the wanted file in a zip container and if it has the right size. 
	 * @param fName path to container
	 * @param name wanted file
	 */
	private void checkZip(String fName, String name) {
		File f = new File(fName);
		if (!f.exists())
			return;
		try(ZipFile zipFile = new ZipFile(f)){
			ZipEntry entry = findZipEntry(zipFile, name);
			if (entry != null){
				res = calcRes(entry.getSize(), entry.getName());
			}
		} catch (IOException exp) {
			log.error("failed to get size for file", name, "from", fName);
		}
	}

	/**
	 * Try to unzip the file contained in a zip file.
	 * @param zipFile
	 * @param entry
	 * @throws IOException
	 */
	private void extractFromZip(String fName, String name) throws IOException {
		try (ZipFile zipFile = new ZipFile(fName)) {
			ZipEntry entry = findZipEntry(zipFile, name);
			if (entry != null) {
				InputStream is = zipFile.getInputStream(entry);
				log.info("extracting data for " + entry.getName() + " from " + zipFile.getName());
				buffer = ByteBuffer.allocate((int) entry.getSize());
				byte[] ioBuffer = new byte[1024];
				int len = is.read(ioBuffer);
				while (len != -1) {
					buffer.put(ioBuffer, 0, len);
					len = is.read(ioBuffer);
				}
			} else {
				throw new FileNotFoundException(name);
			}
			read = true;
		} 
	}

	/**
	 * calculate the resolution of the hgt file. size should be exactly 2 * (res+1) * (res+1) 
	 * @param size number of bytes
	 * @param fname file name (for error possible message)
	 * @return resolution (typically 1200 for 3'' or 3600 for 1'')
	 */
	private int calcRes(long size, String fname) {
		long numVals = (long) Math.sqrt(size / 2d);
		if (2 * 1801 * 3601 == size) {
			numPixelsX = 1801;
			numPixelsY = 3601;
			return 3600;
		}
		if (2 * numVals * numVals == size) {
			numPixelsX = (int) numVals;
			numPixelsY = (int) numVals;
			return (int) (numVals - 1);
		}
		log.error("file", fname, "has unexpected size", size, "and is ignored");
		return -1;
	}

	/**
	 * HGT files are organised as a matrix of n*n (short) values giving the elevation in metres.
	 * Invalid values are coded as 0x8000 = -327678 = Short.MIN_VALUE.
	 * @param x index for column west to east 
	 * @param y index for row north to south
	 * @return the elevation value stored in the file or 0 if 
	 */
	public short ele(int x, int y) {
		if (!read && path != null) {
			prepRead();
		}
		if (buffer == null)
			return 0;
		assert (x >= 0 && x < numPixelsX && y >= 0 && y < numPixelsY) : 
			"wrong x/y value for res" + numPixelsX + "x" + numPixelsY + " x=" + x + " y=" + y;
		count++;
		return buffer.getShort(2 * ((numPixelsY - 1 - y) * numPixelsX + x));
		
	}

	/**
	 * @return the resolution to use with this file, -1 is return if file is invalid
	 */
	public int getRes() {
		return res;
	}
	
	@Override
	public String toString() {
		return fileName + " (" + count + " reads) " + res ;
	}

	/**
	 * Return memory to GC. 
	 * @return true if heap memory was freed.
	 */
	public boolean freeBuf() {
		if (buffer == null)
			return false;
		buffer = null;
		read = false;
		return true;
	}

	public void prepRead() {
		if (!read && path != null) {
			try {
				if (count == 0)
					log.info("allocating buffer for", fileName);
				else 
					log.warn("re-allocating buffer for", fileName);
				if (path.endsWith(".zip"))
					extractFromZip(path, fileName);
				else {
					try (FileInputStream is = new FileInputStream(path)) {
						buffer = is.getChannel().map(READ_ONLY, 0, is.getChannel().size());
						read = true;
					}
				}
			} catch (FileNotFoundException e) {
				throw new MapFailedException("previously existing file is missing: " + path);
			} catch (IOException e) {
				log.error("failed to create buffer for file", path);
			}

		}
	}

	public int getResX() {
		return numPixelsX - 1;
	}

	public int getResY() {
		return numPixelsY - 1;
	}
	
}
