/**
 * Copyright: Copyright (c) 2006 Company: MBARI
 * 
 * @author Andrew C. Chase
 */

package org.mbari.vdm;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.Index;
import ucar.ma2.IndexIterator;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Dimension;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFileWriteable;

/* This library can be found at http://sourceforge.net/projects/timeandmoney/ */
import com.domainlanguage.timeutil.Clock;
import com.domainlanguage.timeutil.SystemClock;

public class NetcdfHelper {

    private static final float NO_DATA_VALUE = -99999;
    private static String TIME_ZONE_ID = "America/Los_Angeles";

    /**
     * @param gridName
     *            The grid file to convert
     * @param newNcFile
     *            The location to save the nc file to
     * @param gridDataTitle
     *            The name of the data that is in the ASCII grid. Null value
     *            okay
     * @param globalAttributes
     *            A collection of key value pairs (of type String:String) that
     *            will be added to the netcdf file as global attributes. Null
     *            value okay
     * @throws IllegalArgumentException
     * @throws IOException
     */
    public static void gridToNetcdf(File gridFile, File newNcFile, String gridDataTitle,
        HashMap globalAttributes, HashMap gridAttributes) throws IllegalArgumentException,
        IOException {
        if (gridFile == null || newNcFile == null || gridDataTitle == null) {
            throw new NullPointerException(
                "Both files and the gridDataTitle must all be non-null values");
        }
        NetcdfFileWriteable writeableNc = NetcdfFileWriteable.createNew(
            newNcFile.getAbsolutePath(), false);
        NetcdfHelper.addGlobalAttributes(writeableNc, globalAttributes);

        BufferedReader reader = new BufferedReader(new FileReader(gridFile));
        AsciiGridHeader header = AsciiGridHeader.parseHeader(reader);
        double[] latArray = createLatArray(header);
        double[] lonArray = createLonArray(header);

        writeableNc.addDimension("latitude", header.nrows);
        writeableNc.addDimension("longitude", header.ncols);
        Group rootGroup = writeableNc.getRootGroup();
        writeableNc.addVariable("latitude", DataType.DOUBLE, new Dimension[]{rootGroup
            .findDimension("latitude")});
        writeableNc.addVariableAttribute("latitude", "units", "degrees_north (+N/-S)");
        writeableNc.addVariable("longitude", DataType.DOUBLE, new Dimension[]{rootGroup
            .findDimension("longitude")});
        writeableNc.addVariableAttribute("longitude", "units", "degrees_east (+E/-W)");
        writeableNc.addVariable(gridDataTitle, DataType.FLOAT, new Dimension[]{
            rootGroup.findDimension("latitude"), rootGroup.findDimension("longitude")});
        if (gridAttributes != null) {
            if(header.NODATA_value != 0){
               //need to cast to float because datatype is float above
//             gridAttributes.put("missing_value", "" + ((float)header.NODATA_value));
                gridAttributes.put("missing_value", "" + NO_DATA_VALUE);
            }
            addVariableAttributes(writeableNc, gridDataTitle, gridAttributes);
        }
        writeableNc.create();

        Array lonNcArray = createNcArray(lonArray);
        Array latNcArray = createNcArray(latArray);
        Array gridArray = getAsciiGridValues(reader, header);
        lookForZeroes(gridArray);
        
        try {
            writeableNc.write("latitude", latNcArray);
            writeableNc.write("longitude", lonNcArray);
            writeableNc.write(gridDataTitle, gridArray);
        } catch (InvalidRangeException rangeE) {
            rangeE.printStackTrace();
            throw new RuntimeException(rangeE);
        }
        writeableNc.close();
    }

    //for testing purposes...
    private static void lookForZeroes(Array gridArray) {
        IndexIterator iterator = gridArray.getIndexIterator();
        while(iterator.hasNext()){
            if(iterator.getFloatNext() == 0){
                System.out.println("zero value found!");
                System.out.println(iterator.getCurrentCounter());
            }
        }
    }

    private static void addGlobalAttributes(NetcdfFileWriteable writeableNc,
        HashMap globalAttributes) {
        Clock.setDefaultTimeZone(TimeZone.getTimeZone(TIME_ZONE_ID));
        Clock.setTimeSource(SystemClock.timeSource());
        if (globalAttributes == null || !globalAttributes.containsKey("creationDate")) {
            writeableNc.addGlobalAttribute("creationDate", Clock.today().toString("MM/dd/yyyy"));
        }
        if (globalAttributes == null || !globalAttributes.containsKey("lastModified")) {
            writeableNc.addGlobalAttribute("lastModified", Clock.today().toString("MM/dd/yyyy"));
        }
        if (globalAttributes != null) {
            for (Iterator iter = globalAttributes.keySet().iterator(); iter.hasNext();) {
                String key = (String) iter.next();
                String value = (String) globalAttributes.get(key);
                writeableNc.addGlobalAttribute(key, value);
            }
        }
    }

    private static void addVariableAttributes(NetcdfFileWriteable writeableNc, String variableName,
        HashMap variableAttributes) {
        for (Iterator iter = variableAttributes.keySet().iterator(); iter.hasNext();) {
            String attributeName = (String) iter.next();
            String attributeValue = (String) variableAttributes.get(attributeName);
            writeableNc.addVariableAttribute(variableName, attributeName, attributeValue);
        }
    }

    private static Array getAsciiGridValues(BufferedReader reader, AsciiGridHeader header)
        throws IOException {
        System.out.println("in getAsciiGridValues");
        System.out.println("header no_data value = "+ header.NODATA_value);
        Array array = Array.factory(DataType.FLOAT, new int[]{header.nrows, header.ncols});
        Index arrayIndex = array.getIndex();
        String[] values;
        for (int i = 0; i < header.nrows; i++) {
            //first remove any leading whitespace
            values = reader.readLine().trim().split("\\s+");
            for (int j = 0; j < header.ncols; j++) {
                if(values[j].length() == 0){
                    System.out.println("Skipping a cell because no data available");
                    System.out.println("row = " + i + ", column = " + j);
                    continue;
                }
                arrayIndex.set(i, j);
                float value = Float.parseFloat(values[j]);
                if(value == header.NODATA_value){
                    value = NO_DATA_VALUE;
                }
                array.setFloat(arrayIndex, value);
            }
        }
        System.out.println("finished with getAsciiGridValues");
        return array;
    }

    private static Array createNcArray(double[] array) {
        Array retval = Array.factory(DataType.DOUBLE, new int[]{array.length});
        Index index = retval.getIndex();
        for (int i = 0; i < array.length; i++) {
            index.set(i);
            retval.setDouble(index, array[i]);
        }
        return retval;
    }

    private static double[] createGridArray(int size, double start, double cellsize) {
        double[] retval = new double[size];
        for (int i = 0; i < size; i++) {
            retval[i] = start + (cellsize * i);
        }
        return retval;
    }

    private static double[] createLonArray(AsciiGridHeader header) {
        return createGridArray(header.ncols, header.xllcorner, header.cellsize);
    }

    private static double[] createLatArray(AsciiGridHeader header) {
        return createGridArray(header.nrows, header.yllcorner + (header.cellsize * header.nrows),
            -header.cellsize);
    }

    /**
     * This method is a convenience method for use within the VDM project, makes
     * assumptions about locations of files.
     * 
     * @param gridName
     * @throws IOException
     * @throws IllegalArgumentException
     */
    public static void convertAsciiToNetcdfForVDM(String gridName, HashMap gAtts, String vName,
        HashMap vAtts) throws IllegalArgumentException, IOException {
        if (!gridName.endsWith("asc")) {
            throw new IllegalArgumentException("Must be an ascii raster grid with extension .asc");
        }
        File gridFile = new File( NetcdfHelper.class.getResource("/grids").getFile(), gridName);
        String newNcFileName = gridName.substring(0, gridName.lastIndexOf('.'));
        File newNcFile = new File(NetcdfHelper.class.getResource("/grids").getFile(), newNcFileName
            + ".nc");
        gridToNetcdf(gridFile, newNcFile, vName, gAtts, vAtts);
    }

    @SuppressWarnings("unchecked")
    public static void convertAsciiDEMToNetcdfForVDM(String gridName)
        throws IllegalArgumentException, IOException {
        HashMap vHash = new HashMap();
        vHash.put("units", "meters");
        vHash.put("positive", "up");
        vHash.put("missing_value", "" + NO_DATA_VALUE);
        vHash.put("long_name", "Elevation");
        HashMap gHash = new HashMap();
        gHash.put("keywords", "dem bathymetry grid");
        gHash.put("title", "DEM Grid from " + gridName);
        gHash.put("creator_name", "Andrew Chase");
        gHash.put("institution", "MBARI");
        convertAsciiToNetcdfForVDM(gridName, gHash, "elevation", vHash);
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        try {
            convertAsciiDEMToNetcdfForVDM("mont_super15.asc");
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("total time = " + ((System.currentTimeMillis() - start)/ 1000) + " seconds");
    }
}

class AsciiGridHeader {

    final int ncols;
    final int nrows;
    final double xllcorner;
    final double yllcorner;
    final double cellsize;
    final float NODATA_value;

    /*
     * This method should only be called by the parseHeader method.
     */
    private AsciiGridHeader(int ncols, int nrows, double xllcorner, double yllcorner,
        double cellsize, float nodata) {
        this.ncols = ncols;
        this.nrows = nrows;
        this.xllcorner = xllcorner;
        this.yllcorner = yllcorner;
        this.cellsize = cellsize;
        this.NODATA_value = nodata;
    }

    public static AsciiGridHeader parseHeader(BufferedReader gridReader) throws IOException {
        Pattern p = Pattern.compile("(\\w+)\\s+(-?\\d+.*)");
        Matcher m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        int ncols = Integer.parseInt(m.group(2));
        m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        int nrows = Integer.parseInt(m.group(2));
        m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        double xllcorner = Double.parseDouble(m.group(2));
        m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        double yllcorner = Double.parseDouble(m.group(2));
        m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        double cellsize = Double.parseDouble(m.group(2));
        m = p.matcher(gridReader.readLine());
        if (!m.matches()) {
            throw new RuntimeException("Header does not obey format contract");
        }
        float nodata = Float.parseFloat(m.group(2));
        return new AsciiGridHeader(ncols, nrows, xllcorner, yllcorner, cellsize, nodata);
    }
}
