package uk.co.mccombe.terrain;
import uk.co.mccombe.mapping.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.*;
import java.net.URISyntaxException;
import java.util.zip.*;
import javax.swing.JComponent;
import javax.net.ssl.HttpsURLConnection;

/**
 * DEMReader - an abstract base class  for readers of various forms of Digital Elevation Model data sets
 * 
 * @author Mike McCombe 
 */
public abstract class DEMReader extends PropertyChangeSupport {

    protected DEMReader(JComponent item) throws DataFileException {
        super(item);
        PropertyChangeListener[] listeners = item.getPropertyChangeListeners();
        for (PropertyChangeListener ear : listeners) {
            addPropertyChangeListener(ear);
        }
        addPropertyChangeListener(listener);
        item.addPropertyChangeListener(listener);
        DIRECTORY = TerrainFrame.paths.dataPath();
        File dir = new File(DIRECTORY);
        if (!dir.isDirectory()) {
            boolean madeDirectory = dir.mkdir();
            if (!madeDirectory) {
                throw new DataFileException(String.format("Failed to create data directory %s%n", DIRECTORY));
            }
        }
        try {
            File readMeTxt = new File(DIRECTORY + "ReadMe.txt");
            if (!readMeTxt.isFile()) {
                PrintWriter readMe = new PrintWriter(new FileWriter(readMeTxt));
                readMe.printf(
                        "This directory contains copies of raw compressed data files downloaded from NASA. TerrainTool will re-use the files it needs%n" +
                        "instead of downloading them again, saving a lot of time. However, you can save disk space by manually deleting any files you%n" +
                        "no longer need.%n%n" +
                        "Data is read in zipped form. DO NOT DECOMPRESS THESE FILES.");

                readMe.close();
            }

        } catch (IOException ex) {
            //Do nothing if there's an error writing the ReadMe file
        }
    }

    protected String makename(String ns, double lat, String ew, double lon) {
        return String.format(formatstring(), ns, (int) lat, ew, (int) lon);
    }
/**
 * Calculate terrain height at a specified location using biaxial cubic Lagrangian 
 * interpolation on surrounding points in the tile. This method provides an exact fit
 * at the points defined in the tile and a slightly smoothed surface in between.
 * To speed the calculation up, rows of values from the tile are cached.
 * 
 * @param place -- the LatLong of the place to calculate the height for.
 * @return -- the calculated height at place.
 * @throws MissingDataFileException
 * @throws DataFileException 
 */
    public double getHeight(LatLong place) throws MissingDataFileException, DataFileException {
        double lat = Math.floor(place.lat());
        double lon = Math.floor(place.lon());
        if(lat>=maxLat()){
            throw new DataFileException(String.format("Latitude beyond the northern limit (%4.1fN) of the dataset",maxLat()));
        }
        if(lat<minLat()){
            throw new DataFileException(String.format("Latitude beyond the southern limit (%4.1fS) of the dataset",minLat()));
        }
       
        String ew = eastWest(lon);
        String ns = northSouth(lat);
        lat = Math.abs(lat);
        lon = Math.abs(lon);
        double x0 = tile(place.lon());
        double y0 = (double) (recordlength() - 1) - tile(place.lat());
        int xtile = (int) x0;
        int ytile = (int) y0;
        String pagename = makename(ns, (int) lat, ew, (int) lon);
        double[] h = new double[3];
        double[] x = new double[3];
        double[] w = new double[3];
        double[] y = new double[3];
        int k = Math.max(ytile - 1, 0);
        int m = 0;
        while (m < 3 && k < recordlength() && k < ytile + 3) {
            CacheEntry page = getRow(pagename, k);
            if (page == null) {
                return MISSING;
            }
            int i = 0;
            int j = xtile;
            while (i < 3 && j < recordlength() && j < xtile + 4) {
                int v = page.getValue(j);
                if (v != missingValue()) {
                    x[i] = (double) j;
                    h[i] = (double) v;
                    i++;
                } else {
                    missing++;
                }
                j++;
            }
            j = xtile - 1;
            while (i < 3 && j >= 0 && j > xtile - 3) {
                int v = page.getValue(j);
                if (v != missingValue()) {
                    x[i] = (double) j;
                    h[i] = (double) v;
                    i++;
                } else {
                    missing++;
                }
                j--;
            }
            if (i == 3) {
                java.awt.geom.Point2D.Double p0 = new java.awt.geom.Point2D.Double(x[0], h[0]);
                java.awt.geom.Point2D.Double p1 = new java.awt.geom.Point2D.Double(x[1], h[1]);
                java.awt.geom.Point2D.Double p2 = new java.awt.geom.Point2D.Double(x[2], h[2]);
                y[m] = lagrangian(x0, p0, p1, p2);
                w[m] = (double) k;
                m++;
            }
            k++;
        }
        if (m == 3) {
            java.awt.geom.Point2D.Double q0 = new java.awt.geom.Point2D.Double(w[0], y[0]);
            java.awt.geom.Point2D.Double q1 = new java.awt.geom.Point2D.Double(w[1], y[1]);
            java.awt.geom.Point2D.Double q2 = new java.awt.geom.Point2D.Double(w[2], y[2]);
            double height = lagrangian(y0, q0, q1, q2);
            resultcount++;
            return height;
        }
        return MISSING;
    }

    protected CacheEntry getRow(String name, int ytile) throws MissingDataFileException, DataFileException {
        tries++;
        cycle++;
        String shortname = String.format("%s#%04d", name, ytile);
        if (cacheEnable) {
            CacheEntry page = cache.get(shortname);
            if (page != null) {
                hits++;
                page.setLastUsed();
                return page;
            }
        }
// Record not in cache or cache not enabled
        try {
            String filename = DIRECTORY + name + extn();
            File infile = new File(filename);
            if (!infile.isFile()) {
                String tempname = name + extn();
                try {
                    downloadFile(tempname);
                } catch (IOException | java.security.KeyManagementException | java.security.NoSuchAlgorithmException | java.net.URISyntaxException e) {
                    throw new MissingDataFileException(String.format("Unable to dowload missing file %s%n", tempname));
                }
            }
            in = new java.util.zip.ZipInputStream(new FileInputStream(infile));
            String entryname = "";
            do {
                ZipEntry entry = in.getNextEntry();
                if(entry==null) throw new DataFileException(String.format("ZIP file %s does not contain expected entry %s",filename,zipEntryName(name)));
                entryname = entry.getName();
            } while (!entryname.equalsIgnoreCase(zipEntryName(name)));
            int recordno = 0;
            try {
                int[] heights;
                while (true) {
                    heights = readRecord();
                    if (recordno == ytile) {
                        break;
                    }
                    recordno++;
                }
                if (cacheEnable) {
                    if (cache.size() >= MAX_CACHE_SIZE) {
//Find the oldest entry and remove it
                        CacheEntry oldest = null;
                        long age = cycle;
                        for (CacheEntry test : cache.values()) {
                            if (test.lastUsed() < age) {
                                age = test.lastUsed();
                                oldest = test;
                            }
                        }
                        String key = oldest.getName();
                        cache.remove(key);
                    }
                    CacheEntry page = new CacheEntry(shortname, heights);
                    cache.put(shortname, page);
                    return page;
                }
                CacheEntry page = new CacheEntry(shortname, heights);
                return page;
            } catch (EOFException e) {
                throw new MissingDataFileException("Hit end of file");
            }
        } catch (IOException e) {
            throw new MissingDataFileException("Unable to read file - " + e.toString());
        }
    }
/**
 * Read the next record from the current ZipInputStream
 * 
 * @return int[] buffer of length recordlength() containing the decoded next record;
 * @throws IOException 
 */
    public int[] readRecord() throws IOException {
        byte[] buffer = new byte[recordlength() * 2];
        int[] outbuffer = new int[recordlength()];
        int sofar = 0;
        //Keep reading until we have the whole record
        while (sofar < recordlength() * 2) {
            int res = in.read(buffer, sofar, recordlength() * 2 - sofar);
            if (res == -1) {
                break;
            }
            sofar += res;
        }
        for (int i = 0; i < recordlength(); i++) {
            short temp;
            if(2 * i < sofar) {
                if (littleendian()) {
                    temp = (short) (buffer[2 * i + 1] << 8 | (0xff & buffer[2 * i]));
                } else {
                    temp = (short) (buffer[2 * i] << 8 | (0xff & buffer[2 * i + 1]));
                }
                outbuffer[i] = temp;
            }
            else {
                outbuffer[i] = missingValue();
            }
        }
        return outbuffer;
    }
/**
 * Read the next byte record from the current ZipInputStream
 * 
 * @return byte[] buffer of length recordlength() containing the decoded next record;
 * @throws IOException 
 */
    public byte[] readByteRecord() throws IOException {
        byte[] buffer = new byte[recordlength()];
        int sofar = 0;
        //Keep reading until we have the whole record
        while (sofar < recordlength()) {
            int res = in.read(buffer, sofar, recordlength() - sofar);
            if (res == -1) {
                break;
            }
            sofar += res;
        }
       return buffer;
    }
    /**
     * Calculate the number of the element in the buffer that refers to a particular lat or lon value
     * 
     * @param x the double lat/lon value
     * @return A double value representing the buffer element index
     */
        public double tile(double x) {
        double q = Math.floor(x);
        double r = (recordlength() - 1) * (x - q);
        return r;
    }
    /**
     * Calculate the proportion of a degree represented by a particular element in the buffer
     * 
     * @param tile the integer number of the element
     * @return double number representing a fraction of a degree
     */
    public double frac(int tile) {
        double x = (double) tile / (double) (recordlength() - 1);
        return x;
    }
    /**
     * Download a specific file and sand save it in scratch space on the local machine
     * 
     * @param filename the name of the file to download
     * @throws java.security.KeyManagementException
     * @throws java.security.NoSuchAlgorithmException
     * @throws IOException
     * @throws MissingDataFileException 
     * @throws java.net.URISyntaxException 
     */
    protected void downloadFile(String filename) throws java.security.KeyManagementException, java.security.NoSuchAlgorithmException, IOException, MissingDataFileException, URISyntaxException {
        String sitename = getDownloadSiteName();
        if ( !downloadable() || sitename==null) {

            String msg = String.format("TerrainTool needs a data file %s", filename );
            throw new MissingDataFileException(msg);
        }
        String urlString = getDownloadSiteName() + "/" + filename;
        java.net.URI uri = new java.net.URI(urlString);
        java.net.URL url = uri.toURL();
        setMessage(String.format("Downloading data: %s", filename));
        HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
        InputStream ins = con.getInputStream();
        String outfilename = DIRECTORY + filename;
        File outfile = new java.io.File(outfilename);
        DataOutputStream outstream = new DataOutputStream(new FileOutputStream(outfile));
        int bytecount = 0;
        long sofar = 0;
        long guess = 900000;
        while (bytecount != -1) {
            byte[] buffer = new byte[BUFFERLENGTH];
            bytecount = ins.read(buffer);
            if (bytecount > 0) {
                outstream.write(buffer, 0, bytecount);
                sofar += bytecount;
                int progress = (int) (100 * sofar / guess);
                setProgress(progress);
                setMessage(String.format("Downloading data: %s", filename));
            }
        }
        outstream.close();
        ins.close();
        setMessage("");
    }
/**
 * CacheEntry -- A class used to store a row of values 
 */
    protected class CacheEntry {
/**
 * Create a CacheEntry object which contains a named row of the current tile.
 * 
 * @param name The name of the row in the form tileName#rownumber> e.g. "NASADEM_HGT_N51W003#1034"
 * @param buffer an integer buffer of length recordlength() containing the heights for the specified row.
 */
        public CacheEntry(String name, int[] buffer) {
            page_name = name;
            last_used = cycle;
            data = buffer;
        }
/**
 * 
 */
        public void setLastUsed() {
            last_used = cycle;
        }
/**
 * getName() Get the name of the CacheEntry
 * 
 * @return String the name of this CacheEntry
 */
        public String getName() {
            return page_name;
        }
/**
 * 
 * @param i int the index value of the item in the row to fetch
 * @return int the value of the specified element
 */
        public int getValue(int i) {
            return data[i];
        }
/**
 * Find when this CacheEntry was last referenced
 * @return long the cycle number when the entry was last referenced
 */
        public long lastUsed() {
            return last_used;
        }
        private long last_used;
        private final String page_name;
        private final int[] data;
    }
/**
 * Fit an exact polynomial to a set of points and return its value at a specified point
 * @param x double the value at which to evaluate the calculated polynomial
 * @param points a set of java.awt.geom.Point2D.Double values
 * @return double value of the polynomial
 */
    public static double lagrangian(double x, java.awt.geom.Point2D.Double... points) {
        int n = points.length;
        double tot = 0.0;
        for (int i = 0; i < n; i++) {
            double prod = 1.0;
            for (int j = 0; j < n; j++) {
                if (j != i) {
                    prod *= (x - points[j].getX()) / (points[i].getX() - points[j].getX());
                }
            }
            tot += prod * points[i].getY();
        }
        return tot;
    }

    public long hits() {
        return hits;
    }

    public long resultcount() {
        return resultcount;
    }

    public long tries() {
        return tries;
    }

    public long missing() {
        return missing;
    }

    public void resetCounts() {
        hits = 0;
        tries = 0;
        missing = 0;
        resultcount = 0;
    }

    protected void setMessage(String msg) {
        if (!msg.equals(lastMessage)) {
            firePropertyChange("message", lastMessage, msg);
        }
        lastMessage = msg;
    }

    protected void setProgress(int val) {
        int v = Math.min(100, val);
        v = Math.max(0, v);
        if (lastValue != v) {
            firePropertyChange("progress", lastValue, v);
        }
        lastValue = v;
    }

    protected String getProperty(TerrainProperties propertyName) {
        return TerrainFrame.properties.get(propertyName);
    }

    @Override
    public PropertyChangeListener[] getPropertyChangeListeners() {
        PropertyChangeListener[] res = new PropertyChangeListener[1];
        res[0] = listener;
        return res;
    }
    private PropertyChangeListener listener = new java.beans.PropertyChangeListener() {

        @Override
        public void propertyChange(java.beans.PropertyChangeEvent evt) {
            String propertyName = evt.getPropertyName();
            String propertyValue = evt.getNewValue().toString();
/*            if (propertyName.equalsIgnoreCase("download")) {
                download = propertyValue.equalsIgnoreCase("true");
            } */
            if (propertyName.equalsIgnoreCase("message")) {
                lastMessage = propertyValue;
            }
        }
    };
   public abstract String datasetName();
    public abstract boolean downloadable();
    public abstract int recordlength();
    public abstract String formatstring();
    public abstract String extn();
    public abstract boolean littleendian();
    public abstract int missingValue();
    public abstract String[] copyright() ;
    protected abstract void setDownloadSiteName(String s) ;
    protected abstract String getDownloadSiteName();
    protected abstract String eastWest(double lon);
    protected abstract String northSouth(double lat);
    public abstract double maxLat(); //Maximum latitude for dataset 
    public abstract double minLat(); //Minimum latitude for dataset
    public String zipEntryName(String name){
        return name + ".hgt";
    }
    public String numEntryName(String name){
        return "";
    }
    protected static java.util.zip.ZipInputStream in;
    protected  String DIRECTORY = "";
    protected long cycle = 0;
    protected java.util.HashMap<String, CacheEntry> cache = new java.util.HashMap<>();
    protected boolean cacheEnable = true;
    protected static final int MAX_CACHE_SIZE = 16;
    protected long hits = 0;
    protected long tries = 0;
    protected long missing = 0;
    protected long resultcount = 0;
    protected static final double MISSING = -32768.0;
    protected static final int BUFFERLENGTH = 1024;  
    protected String lastMessage = "";
    protected int lastValue = 0;
    protected static final java.util.Locale LOCALE = java.util.Locale.UK; //Force use of UK locale for number formatting
    protected boolean useLegacy = false ;
    protected String[] copyright = {"Copyright statement is missing"};
}
