package ucar.nc2.adde;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.thredds.ThreddsDataFactory;
import ucar.nc2.dataset.*;
import ucar.nc2.station.StationObsDataset;
import ucar.nc2.station.StationDatasetHelper;

import ucar.unidata.geoloc.LatLonRect;
import ucar.unidata.geoloc.LatLonPointImpl;

import java.io.IOException;
import java.util.*;

import edu.wisc.ssec.mcidas.adde.AddePointDataReader;
import edu.wisc.ssec.mcidas.adde.AddeException;
import edu.wisc.ssec.mcidas.McIDASUtil;
import thredds.catalog.InvAccess;
import thredds.catalog.InvDataset;
import thredds.catalog.InvProperty;
import thredds.catalog.InvDatasetImpl;

/**
 * An adde "point" dataset.
 */
public class AddeStationDataset extends NetcdfDataset implements StationObsDataset {
  private StationDatasetHelper helper;
  private ArrayList memberNames = new ArrayList();
  private HashMap hashVars = new HashMap(100);
  private HashMap hashStationData = new HashMap(10000);
  private StationDB stationDB;
  private String stationDBlocation;

  private Structure topStructure;
  private Variable stationNameVar;
  private Structure stationObsVar;

  private boolean debugHead = false;

  /**
   * Open an ADDE Station Dataset from an InvAccess, which must be type ADDE and Station.
   *
   * @param access open Invdataset from this access.
   * @throws IOException
   */
  public AddeStationDataset(InvAccess access, ucar.nc2.util.CancelTask cancelTask) throws IOException {
    super();
    this.location = access.getStandardUrlName();

    // see if we have a stationDB file
    InvDataset invds = access.getDataset();
    String pv = invds.findProperty( "_StationDBlocation");
    if (pv != null) {
      stationDBlocation = InvDatasetImpl.resolve( invds, pv);
    }

    init();

    finish();
    ThreddsDataFactory.annotate( access.getDataset(), this);
    finish();

    helper = new StationDatasetHelper(this);
  }

  /**
   * Open an ADDE Station Dataset.
   *
   * @param location location of file. This is a URL string, or a local pathname.
   * @throws IOException
   */
  public AddeStationDataset(String location, ucar.nc2.util.CancelTask cancelTask) throws IOException {
    super();
    this.location = location;

    init();
    finish();

    helper = new StationDatasetHelper(this);
  }


  public void close() { }

  private double[] scaleFactor;
  private int nparams = 0;

  static AddePointDataReader callAdde( String request) throws IOException {
    try {
      System.out.println("Call ADDE request= "+request);
      long start = System.currentTimeMillis();
      AddePointDataReader reader = new AddePointDataReader( request);
      System.out.println(" took= "+(System.currentTimeMillis()-start)+" msec");

      return reader;

    } catch (AddeException e) {
      e.printStackTrace();
      throw new IOException(e.getMessage());
    }
  }

  private void init() throws IOException {
    topStructure = new StructureDS(this, null, null, "stations", "*", null, "Stations");
    topStructure.setCaching(true);
    addVariable( null, topStructure);

    stationNameVar = new VariableDS(this, null, topStructure, "id", DataType.STRING, "", null, "Station id");
    stationNameVar.setCaching(true);
    topStructure.addMemberVariable( stationNameVar);

    stationObsVar = new StructureDS(this, null, topStructure, "stationObs", "*", null, "Station observations");
    stationObsVar.setCaching(true);
    topStructure.addMemberVariable( stationObsVar);

    try {
      AddePointDataReader reader = callAdde( location);

      String[] params = reader.getParams();
      String[] units = reader.getUnits();
      int[] scales  = reader.getScales();
      scaleFactor = new double[ params.length];

      if (debugHead) System.out.println(" Param  Unit Scale");
      for (int i = 0; i < params.length; i++) {
        memberNames.add( params[i]);

        if (debugHead) System.out.println(" "+params[i]+" "+units[i]+" "+scales[i]);
        if (scales[i] != 0)
           scaleFactor[i] = 1.0/Math.pow(10.0, (double) scales[i]);

        DataType dt = null;
        if ("CHAR".equals(units[i]))
          dt = DataType.STRING;
        else if (scaleFactor[i] == 0)
          dt = DataType.INT;
        else
          dt = DataType.DOUBLE;

        String unitString = null;
        if ((units[i] != null) && (units[i].length() > 0))
          unitString = visad.jmet.MetUnits.makeSymbol(units[i]);

        Variable v = new AddeVariable(this, stationObsVar, params[i], dt, "", unitString, null, i);
        v.setCaching(true); // all variables are cached

        stationObsVar.addMemberVariable( v);
        hashVars.put( v.getShortName(), v);
      }

    } catch (AddeException e) {
      e.printStackTrace();
      throw new IOException(e.getMessage());
    }
  }


  private int [][] readStationObs(String stationName) throws IOException {
    try {
      AddePointDataReader reader = callAdde( location+"&num=all&select='ID "+stationName+"'");

      int [][] stationObsData = reader.getData();
      int nparams = stationObsData.length;
      int nobs = stationObsData[0].length;
      System.out.println(" nparams= "+nparams+" nobs=" + nobs);
      System.out.println(" size= "+(nparams * nobs * 4)+" bytes");
      hashStationData.put(stationName, stationObsData);

     return stationObsData;

    } catch (AddeException e) {
      e.printStackTrace();
      throw new IOException(e.getMessage());
    }
  }

  private int[][] getData(String stationName) {
    if (stationName == null) throw new IllegalArgumentException("AddeNetcdfFile getData: null stationName");

    int[][] data = (int[][]) hashStationData.get( stationName);
    if (data == null) {
      try {
        data = readStationObs( stationName);
      } catch (IOException e) {
        e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
      }
    }
    return data;
  }

   Array makeStationObsArray(String stationName) throws IOException {
    int [][] stationObsData = getData(stationName);
    int nparams = stationObsData.length;
    int nobs = stationObsData[0].length;

    Array result = Array.factory( DataType.STRUCTURE.getPrimitiveClassType(), new int[] {nobs});
    IndexIterator ii = result.getIndexIterator();
    for (int row=0; row<nobs; row++){
      StructureData sd = new AddeStructureData(this, stationObsVar, stationName, nparams, row);
      ii.setObjectNext( sd);
    }
    return result;
  }

  //////////////////////////////////////////////////////////////////////////////////////////
  // ncfile I/O implemntation
  // top structure - get stations
  public Array readData(ucar.nc2.Variable v, List section) throws IOException, InvalidRangeException  {
    if (stationDB == null)
      readStations();

    List stations = stationDB.getStations();
    int nstations = stations.size();
    Array result = Array.factory( DataType.STRUCTURE.getPrimitiveClassType(), new int[] {nstations});
    IndexIterator ii = result.getIndexIterator();
    for (int row=0; row<nstations; row++){
      Station s = (Station) stations.get( row);
      StructureData sd = new StationStructData(this, topStructure, stationNameVar, s.getName(), stationObsVar);
      ii.setObjectNext( sd);
    }
    return result;
  }

  // this is for reading variables that are members of structures
  public Array readMemberData(ucar.nc2.Variable v, List section, boolean flatten) throws IOException, InvalidRangeException  {
    if (stationDB == null)
      readStations();

    if (v.getName().equals(stationObsVar.getName())) {
      Range stationRange = (Range) section.get(0);
      Array stationArray = Array.factory( DataType.STRUCTURE.getPrimitiveClassType(), new int[] {stationRange.length()});

      List stations = stationDB.getStations();
      int nstations = stations.size();

      IndexIterator stationIter = stationArray.getIndexIterator();

      for (int row=stationRange.first(); row<=stationRange.last(); row+=stationRange.stride()){
        Station s = (Station) stations.get( row);
        StructureData sd = new StationStructData(this, topStructure, stationNameVar, s.getName(), stationObsVar);
        stationIter.setObjectNext( sd);
      }
      return stationArray;
    }

    return null;
  }

  /** Debug info for this object. */
  protected String toStringDebug(Object o) {
    return null;
  }

  ////////////////////////////////////////////////////////////////////////////////////////////
  // package private - used by other classes in the package
  List getMemberNames() { return memberNames; }

  // fetching data
  byte getByte(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getByte( stationName, v.getParam(), row);
  }

  short getShort(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getShort( stationName, v.getParam(), row);
  }

  int getInt(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getInt( stationName, v.getParam(), row);
  }

  float getFloat(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getFloat( stationName, v.getParam(), row);
  }

  double getDouble(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getDouble( stationName, v.getParam(), row);
  }

  String getString(String stationName, String memberName, int row) {
    AddeVariable v = (AddeVariable) hashVars.get( memberName);
    return getString( stationName, v.getParam(), row);
  }

  byte getByte(String stationName, int param, int row) {
    return (byte) getInt(stationName, param, row);
  }

  short getShort(String stationName, int param, int row) {
    return (short) getInt(stationName, param, row);
  }

  int getInt(String stationName, int param, int row) {
    int[][] data = getData( stationName);
    int v = data[param][row];
    if (v == McIDASUtil.MCMISSING)
      return MISSING_VALUE_INT;
    return v;
  }

  float getFloat(String stationName, int param, int row) {
    return (float) getDouble( stationName, param, row);
  }

  double getDouble(String stationName, int param, int row) {
    int[][] data = getData( stationName);
    int v = data[param][row];
    if (v == McIDASUtil.MCMISSING)
      return MISSING_VALUE_DOUBLE;

    if (scaleFactor[param] != 0.0)
        return v * scaleFactor[param];

    return (double) v;
  }

  String getString(String stationName, int param, int row) {
    int[][] data = getData( stationName);
    return McIDASUtil.intBitsToString(data[param][row]);
  }

  private int MISSING_VALUE_INT = -9999; // ??
  private double MISSING_VALUE_DOUBLE = Double.NaN;

  int findParam(String memberName) {
    for (int i = 0; i < memberNames.size(); i++) {
      String s = (String) memberNames.get(i);
      if (memberName.equals(s)) return i;
    }
    return -1;
  }

  Variable getVariable(int param) {
    return (Variable) hashVars.get((String) memberNames.get(param));
  }

  private int[] scalarShape = new int[0];
  private Index scalarIndex = new Index0D(scalarShape);

  Array getDataArray(String stationName, int param, int row) {

    Variable v = getVariable(param);
    if (v.getDataType() == DataType.STRING) {
      Array result = Array.factory(DataType.STRING.getPrimitiveClassType(), scalarShape);
      result.setObject( scalarIndex, getString(stationName, param, row));
      return result;
    }

    if (v.getDataType() == DataType.INT) {
      Array result = Array.factory(DataType.INT.getPrimitiveClassType(), scalarShape);
      result.setInt( scalarIndex, getInt(stationName, param, row));
      return result;
    }

    if (v.getDataType() == DataType.DOUBLE) {
      Array result = Array.factory(DataType.DOUBLE.getPrimitiveClassType(), scalarShape);
      result.setDouble( scalarIndex, getDouble(stationName, param, row));
      return result;
    }

    return null;
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////////

  // heres the StationObsDataset implementation
  public Date getStartDate() {
    return null;  //To change body of implemented methods use File | Settings | File Templates.
  }

  public Date getEndDate() {
    return null;  //To change body of implemented methods use File | Settings | File Templates.
  }

  private LatLonRect rect = null;
  public LatLonRect getBoundingBox() {
    if (rect != null) return rect;

    // see if we have a stationDB file
    Attribute bbAtt = findGlobalAttributeIgnoreCase( "LatLonBoundingBox");
     if ((bbAtt != null) && (bbAtt.getLength() == 4))  {
       double begLat = bbAtt.getNumericValue(0).doubleValue();
       double begLon = bbAtt.getNumericValue(1).doubleValue();
       double deltaLat = bbAtt.getNumericValue(2).doubleValue();
       double deltaLon = bbAtt.getNumericValue(3).doubleValue();

       rect = new LatLonRect( new LatLonPointImpl(begLat, begLon), deltaLat, deltaLon);


     } else // otherwise, helper constructs from the station locations
      rect = helper.getBoundingBox();

     return rect;
  }

  public List getStations() throws IOException {
    if (stationDB == null)
      readStations();
    return stationDB.getStations();
  }

  public List getStations(LatLonRect boundingBox) throws IOException {
    return helper.getStations(boundingBox);
  }

  public StationObsDataset.Station getStation(String id) {
    return helper.getStation(id);
  }

  public int getStationObsCount(StationObsDataset.Station s) {
    int[][] data = (int[][]) hashStationData.get( s.getName());
    return ((data == null) || (data.length == 0)) ? -1 : data[0].length;
  }

  // get a list of station obs for this station
  public List getStationObs(StationObsDataset.Station s) throws IOException {
    Array stationData = makeStationObsArray(s.getName());

    ArrayList stationObs = new ArrayList();
    IndexIterator ii = stationData.getIndexIterator();
    while (ii.hasNext()) {
      stationObs.add( new StationObs( (Station) s, new Date(), (StructureData) ii.getObjectNext()));
    }
    return stationObs;
  }

  public List getStationObs(StationObsDataset.Station s, Date start, Date end) throws IOException {
    return helper.getStationObs(s, start, end); // LOOK
  }

  public List getStationObs(List stations) throws IOException {
    return helper.getStationObs(stations); // LOOK
  }

  public List getStationObs(List stations, Date start, Date end) throws IOException {
    return helper.getStationObs(stations, start, end);
  }

  public List getStationObs(LatLonRect boundingBox) throws IOException {
    return helper.getStationObs(boundingBox);
  }

  public List getStationObs(LatLonRect boundingBox, Date start, Date end) throws IOException {
    return helper.getStationObs(boundingBox, start, end);
  }

  public void sortByTime(List stationObs) {
    helper.sortByTime(stationObs);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////
  private void readStations() throws IOException {
    // see if we have a stationDB file
    //String stnDBlocation = findAttValueIgnoreCase(null, "_StationDBlocation", null);
    try {
      if (stationDBlocation != null)
        stationDB = new StationDB(stationDBlocation);
    } catch (IOException ioe) {
      System.out.println("++ AddeStationDataset cant find stationDBlocation= "+ stationDBlocation);
    }

    if (stationDB == null) // otherwise, we have to read all records from server !!!
      stationDB = new StationDB(location, true);
  }

  private class StationObs implements StationObsDataset.StationObs {
    private Station s;
    private Date time;
    private StructureData data;

    StationObs( Station s, Date time, StructureData data) {
      this.s = s;
      this.time = time;
      this.data = data;
    }

    public StationObsDataset.Station getStation() {
      return s;
    }

    public Date getTime() {
      return time;
    }

    public StructureData getData() throws IOException  {
      return data;
    }
  }

//////////////////////////////////////////////////////////////////

  static void test(String urlString) {
    try {
      long start = System.currentTimeMillis();
      System.out.println(" get "+urlString);

      AddePointDataReader reader = new AddePointDataReader( urlString);

      System.out.println(" took= "+(System.currentTimeMillis()-start)+" msec");
      System.out.println(reader.toString());

      System.out.println(" Param  Unit Scale");

      String[] params = reader.getParams();
      String[] units = reader.getUnits();
      int[] scales = reader.getScales();
      for (int i = 0; i < params.length; i++) {
        System.out.println(" "+params[i]+" "+units[i]+" "+scales[i]);
      }

      int[][] data = reader.getData();
      System.out.println(" nparams= "+params.length);
      System.out.println(" n= "+data.length);
      System.out.println(" m= "+data[0].length);

    } catch (AddeException e) {
      e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
    }
  }

  static public void main( String[] args) {


    //new AddePointDataset("adde://adde.ucar.edu/point?group=rtptsrc&descr=sfchourly&select='row 1'&num=all&param=id lat lon zs&pos=all");
    // new AddePointDataset("adde://adde.ucar.edu/point?group=rtptsrc&descr=sfchourly&num=all&param=ID");
    test("adde://adde.ucar.edu/point?group=rtptsrc&descr=sfchourly&num=10");
    //new AddePointDataset("adde://adde.ucar.edu/point?group=rtptsrc&descr=sfchourly&num=all&param=ID");
  }


}
