
package org.mbari.vdm.expd;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.TimeZone;

import org.mbari.vdm.CsvHelper;
import org.mbari.vdm.UiFeedback;
import org.mbari.vdm.VdmLine;

import ucar.ma2.Array;
import ucar.ma2.ArrayInt;
import ucar.ma2.DataType;
import ucar.ma2.Index;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Dimension;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFile;
import ucar.nc2.NetcdfFileWriteable;
import ucar.nc2.Variable;
import ucar.unidata.geoloc.LatLonPoint;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.LatLonRect;

import com.domainlanguage.time.CalendarDate;
import com.domainlanguage.time.TimePoint;
import com.domainlanguage.time.TimeSource;
import com.domainlanguage.timeutil.Clock;

public class NetCdfCreator {

    /**
     * Get the LatLonRect that is defined by the minimum and maximum values
     * within the Latitude and Longitude Dimensions of the given ncFile. An
     * illegal argument exception will be thrown if the ncFile does not have
     * Latitude and Longitude Dimensions.
     * 
     * @param ncFile
     * @return
     * @throws IOException
     */
    public static final LatLonRect getNcBaseMapBounds() throws IOException {
        NetcdfFile ncFile = getBaseMap();
        Dimension latDimension = ncFile.getRootGroup().findDimension("latitude");
        Variable latVariable = (Variable) latDimension.getCoordinateVariables().get(0);
        Dimension lonDimension = ncFile.getRootGroup().findDimension("longitude");
        Variable lonVariable = (Variable) lonDimension.getCoordinateVariables().get(0);
        int lastLatIndex = latVariable.getShape()[0] - 1;
        int lastLonIndex = lonVariable.getShape()[0] - 1;
        try {
            String secSpec = "1:" + lastLatIndex + ":" + (lastLatIndex - 1);
            // String secSpec = "1:" + lastLatIndex + ":1";
            double[] latArray = (double[]) latVariable.read(secSpec).get1DJavaArray(double.class);
            secSpec = "1:" + lastLonIndex + ":" + (lastLonIndex - 1);
            // secSpec = "1:" + lastLonIndex + ":1";
            double[] lonArray = (double[]) lonVariable.read(secSpec).get1DJavaArray(double.class);
            LatLonPoint left = new LatLonPointImpl(latArray[0], lonArray[0]);
            LatLonPoint right = new LatLonPointImpl(latArray[1], lonArray[1]);
            return new LatLonRect(left, right);
        } catch (InvalidRangeException e) {
            e.printStackTrace();
            // this should never happen, if it does it is most likely a
            // programming error, so lets throw a runtime exception
            throw new RuntimeException(e);
        }
    }

    /**
     * Get the data array of a named dimension. This has only been tested for
     * Latitude and longitude dimensions. Exception thrown if dimension does not
     * exist.
     * 
     * @param ncFile
     * @param nameOfDimension
     * @return
     * @throws IOException
     */
    public static final DimensionParts getDimensionArray(NetcdfFile ncFile, String nameOfDimension)
        throws IOException {
        if (ncFile == null || nameOfDimension == null) {
            throw new NullPointerException("Both parameters must be non-null.");
        }
        Dimension dimension = ncFile.getRootGroup().findDimension(nameOfDimension);
        Variable dimVariable = (Variable) dimension.getCoordinateVariables().get(0);
        Array array = dimVariable.read();
        return new DimensionParts(dimension, dimVariable, array);
    }

    public static final void addBasicMDVariables(NetcdfFileWriteable ncFile) {
        Clock.setDefaultTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
        TimeSource source = new TimeSource() {

            public TimePoint now() {
                return TimePoint.from(System.currentTimeMillis());
            }
        };
        Clock.setTimeSource(source);
        CalendarDate date = Clock.today();
        ncFile.addGlobalAttribute("creationDate", date.toString("MM/dd/yyyy"));
        ncFile.addGlobalAttribute("lastModified", date.toString("MM/dd/yyyy"));
        ncFile.addGlobalAttribute("description", "Test File");
        ncFile.addGlobalAttribute("title", "Test Data");
        ncFile.addGlobalAttribute("keywords", "test");
    }

    public static final void createNetcdfFromVarsCsv(File varsCsv, UiFeedback ui, VdmLine[] lines)
        throws IOException, InvalidRangeException {
        // TODO just dummying this variable name for now...
        String varsDataName = varsCsv.getName().substring(0, varsCsv.getName().lastIndexOf('.'));
        String newNcFileName = varsCsv.getName().replaceFirst("\\.csv", ".nc");
        File newNcFile = new File(varsCsv.getParent(), newNcFileName);
        NetcdfFileWriteable writeableNc = NetcdfFileWriteable.createNew(
            newNcFile.getAbsolutePath(), true);
        NetcdfFile baseMap = getBaseMap();
        DimensionParts latitudeParts = NetCdfCreator.getDimensionArray(baseMap, "latitude");
        DimensionParts longitudeParts = NetCdfCreator.getDimensionArray(baseMap, "longitude");
        Group rootGroup = writeableNc.getRootGroup();
        writeableNc.addDimension(rootGroup, latitudeParts.dimension);
        writeableNc.addVariable(rootGroup, latitudeParts.variable);
        writeableNc.addDimension(rootGroup, longitudeParts.dimension);
        writeableNc.addVariable(rootGroup, longitudeParts.variable);
        NetCdfCreator.addBasicMDVariables(writeableNc);
        writeableNc.addVariable(varsDataName, DataType.INT, new Dimension[]{
            latitudeParts.dimension, longitudeParts.dimension});
        writeableNc.addVariableAttribute(varsDataName, "units", "count");
        writeableNc.addVariableAttribute(varsDataName, "fillvalue", 0);
        writeableNc.addVariableAttribute(varsDataName, "missing_value", "0");

        writeableNc.create();

        writeableNc.write("latitude", latitudeParts.array);
        writeableNc.write("longitude", longitudeParts.array);
        DepthFilter filter = new TrueFilter();

        // remove all the lines that have a null value for there latitude or
        // longitude
        Array expdArray = createNcArray(lines, filter);

        writeableNc.write(varsDataName, expdArray);
        writeableNc.close();

    }

    private static final NetcdfFileWriteable createEmptyRovCoverageFile(NetcdfFile baseMap,
        String newNcFileName) throws IOException, InvalidRangeException {
        DimensionParts latitudeParts = NetCdfCreator.getDimensionArray(baseMap, "latitude");
        DimensionParts longitudeParts = NetCdfCreator.getDimensionArray(baseMap, "longitude");
        File newNcFile = new File( NetCdfCreator.class.getResource("/expdData").getFile(), newNcFileName
            + ".nc");
        NetcdfFileWriteable writeableNc = NetcdfFileWriteable.createNew(
            newNcFile.getAbsolutePath(), true);
        Group rootGroup = writeableNc.getRootGroup();
        writeableNc.addDimension(rootGroup, latitudeParts.dimension);
        writeableNc.addVariable(rootGroup, latitudeParts.variable);
        writeableNc.addDimension(rootGroup, longitudeParts.dimension);
        writeableNc.addVariable(rootGroup, longitudeParts.variable);
        NetCdfCreator.addBasicMDVariables(writeableNc);

        writeableNc.addVariable("rovcoverage", DataType.INT, new Dimension[]{
            latitudeParts.dimension, longitudeParts.dimension});
        writeableNc.addVariableAttribute("rovcoverage", "units", "meters");
        writeableNc.addVariableAttribute("rovcoverage", "fillvalue", 0);
        writeableNc.addVariableAttribute("rovcoverage", "missing_value", "0");

        writeableNc.create();

        writeableNc.write("latitude", latitudeParts.array);
        writeableNc.write("longitude", longitudeParts.array);
        return writeableNc;
    }

    public static final Array createNcArray(VdmLine[] lines, DepthFilter filter)
        throws IOException, InvalidRangeException {
        System.out.println("sorting...");
        Arrays.sort(lines, ExpdLine.getComparator());
        NetcdfFile baseMap = getBaseMap();
        DimensionParts latitudeParts = NetCdfCreator.getDimensionArray(baseMap, "latitude");
        DimensionParts longitudeParts = NetCdfCreator.getDimensionArray(baseMap, "longitude");

        int[] dimensions = {(int) latitudeParts.variable.getSize(),
            (int) longitudeParts.variable.getSize()};
        Array expdArray = new ArrayInt(dimensions);

        Index expdArrayIndex = expdArray.getIndex();

        LatLonRect mapRect = NetCdfCreator.getNcBaseMapBounds();

        // first, let's loop through the lines array and check how many points
        // are in the mapRect
        int outOfBoundsCounter = 0;
        for (int i = 0; i < lines.length; i++) {
            if (lines[i].getPoint() == null || !mapRect.contains(lines[i].getPoint())) {
                outOfBoundsCounter++;
            }
        }
        System.out.println(outOfBoundsCounter + " points are out of bounds");
        // assuming coordinates are in northwestern hemisphere so that
        // i can do bounded checks of longitude left to right and latitude
        // top to bottom. also assume that lines has been sorted
        double[] latArray = (double[]) latitudeParts.array.copyTo1DJavaArray();
        double[] lonArray = (double[]) longitudeParts.array.copyTo1DJavaArray();
        int yetAnotherCounter = 0;

        // find the offsets into the array that mark the beginning and end of a
        // single line
        // of latitude from the basemap.
        int startOfLatLine = 0, endOfLatLine = 0;
        int mapElevationsCurrentLatitudeIndex = 0;
        float[] mapElevations = null;
        mapElevations = (float[]) baseMap.findTopVariable("elevation").read(
            mapElevationsCurrentLatitudeIndex + ":" + mapElevationsCurrentLatitudeIndex + ",:")
            .copyTo1DJavaArray();
        System.out.println("Elevation array length = " + mapElevations.length);
        int linesIndex = 0;
        int i = 0;
        while (i < latArray.length - 1 && linesIndex < lines.length) {
            if (linesIndex > 430 && linesIndex < 450){
                System.out.println("stop here");
            }
            if (lines[linesIndex].getLat() <= latArray[i]
                && lines[linesIndex].getLat() > latArray[i + 1]) {
                i++;
                startOfLatLine = linesIndex;
                while (i+1 < latArray.length && 
                    linesIndex < (lines.length -1) && lines[linesIndex + 1].getLat() > latArray[i + 1]) {
                    linesIndex++;
                    if (linesIndex > 430 && linesIndex < 450){
                        System.out.println("stop here");
                    }
                }
                endOfLatLine = linesIndex;
                if (linesIndex > 430 && linesIndex < 450){
                    System.out.println("stop here");
                }
                VdmLine[] subArray = new VdmLine[endOfLatLine - startOfLatLine + 1];
                // make a new array so we can re-sort it just based on longitude
                for (int j = 0; j < subArray.length; j++) {
                    subArray[j] = lines[startOfLatLine + j];
                }
                Arrays.sort(subArray, ExpdLine.getLonComparator());
                int subArrayIndex = 0;
                //now that we're in the right spot along the latitude axis, let's find where we fit in on the 
                //longitude axis
                for (int j = 0; j < lonArray.length - 1 && subArrayIndex < subArray.length; j++) {
                    //keep incrementing j until the subarray's longitude is between two longitude values
                    while (subArrayIndex < subArray.length
                        && subArray[subArrayIndex].getLon() > lonArray[j]
                        && subArray[subArrayIndex].getLon() <= lonArray[j + 1]) {
                        expdArrayIndex.set(i, j);
                        int newVal = expdArray.getInt(expdArrayIndex) + 1;
                        subArrayIndex++;
                        if (filter != null) {
                            if (mapElevationsCurrentLatitudeIndex != i) {
                                mapElevationsCurrentLatitudeIndex = i;
                                mapElevations = (float[]) baseMap.findTopVariable("elevation")
                                    .read(
                                        mapElevationsCurrentLatitudeIndex + ":"
                                            + mapElevationsCurrentLatitudeIndex + ",:")
                                    .copyTo1DJavaArray();
                            }
                            // we've already incremented subArrayIndex, so
                            // remember to subtract 1
                            // depths are positive, elevations are negative
                            if (!filter.test(subArray[subArrayIndex - 1], mapElevations[j])) {
                                //if the test fails, do not execute the expdArray.setInt method below,
                                //subArrayIndex has already been incremented, so just continue.
                                continue;
                            }
                        }
                        expdArray.setInt(expdArrayIndex, newVal);
                        yetAnotherCounter++;
                    }
                }
            } else {
// System.out.println("skipping -- else");
                if (lines[linesIndex].getLat() <= latArray[i]) {
                    i++;

                } else {
                    linesIndex++;
                    if(linesIndex > 430 && linesIndex < 450){
                        System.out.println("line index = " + linesIndex + " i = " + i);
                    }
                }
            }
        }

        System.out.println("Number of rov points added to nc file: " + yetAnotherCounter);
        return expdArray;
    }

    public static final void createRovCoverageFile(VdmLine[] lines, NetcdfFile baseMap,
        String newNcFileName, DepthFilter filter) throws IOException, InvalidRangeException {
        Array expdArray = createNcArray(lines, filter);
        NetcdfFileWriteable writeableNc = NetCdfCreator.createEmptyRovCoverageFile(baseMap,
            newNcFileName);
        writeableNc.write("rovcoverage", expdArray);
        writeableNc.close();
    }

    public static NetcdfFile getBaseMap() throws IOException {
        File baseNcFile = new File( NetCdfCreator.class.getResource("/grids").getFile(), "hrbath.nc");
        return NetcdfFile.open(baseNcFile.getAbsolutePath());
    }

    public static void main(String[] args) {

//        String[] tableNames = new String[]{"VentanaCleanNavData", "TiburonCleanNavData"};
        String[] tableNames = new String[]{"VentanaCleanNavData"};
        for (int i = 0; i < tableNames.length; i++) {
            String tableName = tableNames[i];
            BufferedReader expdReader = null;
            try {
                expdReader = new BufferedReader(new FileReader(new File(
                     NetCdfCreator.class.getResource("/expdData").getFile(), tableName + "_filtered.csv")));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                System.exit(1);
            }
            VdmLine[] lines;
            try {
                lines = (VdmLine[]) CsvHelper.readLines(expdReader, "ExpdLine");
                System.out.println("done sorting");
                DepthFilterImpl filter = new DepthFilterImpl();
                // DepthFilter filter = null;
                String filename = "rovcoverage_" + tableName;
                if (filter != null) {
                    filename += "_" + filter.getShortName();
                }
                System.out.println("before creation...");
                NetCdfCreator.createRovCoverageFile(lines, getBaseMap(), filename, filter);
                System.out.println("after creation...");
                filter.printStatistics();
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            } catch (InvalidRangeException e) {
                e.printStackTrace();
                System.exit(1);
            }
            lines = null;
            System.gc();
        }
    }
}

interface DepthFilter{
    boolean test(VdmLine line, float depth);
}

class TrueFilter implements DepthFilter {
    public boolean test(VdmLine line, float depth){
        return true;
    }
}

class DepthFilterImpl implements DepthFilter{

    private static final int allowedDistanceFromBottom = 30;
    private static final int allowedDistanceBelowBottom = 20;
    private long belowBottomCount = 0;
    private long sumOfDepthsBelowBottom = 0;
    private float furthestBelow = 0;

    public boolean test(VdmLine line, float depth) {
        float heightOffBottom = (-1 * depth) - line.getDepth();
        if (heightOffBottom > allowedDistanceFromBottom) {
            return false;
        }
        if (heightOffBottom < -allowedDistanceBelowBottom) {
            belowBottomCount++;
            sumOfDepthsBelowBottom -= heightOffBottom;
            if (heightOffBottom < furthestBelow) {
                furthestBelow = heightOffBottom;
            }
            return false;
        }
        return true;
    }

    public void printStatistics() {
        System.out.println("Total points below bottom: " + belowBottomCount);
        if (belowBottomCount > 0) {
            System.out.println("Average distance below bottom: "
                + (sumOfDepthsBelowBottom / belowBottomCount));
        }
        System.out.println("Furthest point below bottom: " + furthestBelow);
    }

    public String getShortName() {
        return "" + allowedDistanceFromBottom + "_" + allowedDistanceBelowBottom;
    }
}