// $Id: AWIPSConvention.java,v 1.11 2004/12/09 00:17:31 caron Exp $
/*
 * Copyright 1997-2000 Unidata Program Center/University Corporation for
 * Atmospheric Research, P.O. Box 3000, Boulder, CO 80307,
 * support@unidata.ucar.edu.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or (at
 * your option) any later version.
 *
 * This library 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 Lesser
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package ucar.nc2.dataset.conv;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.nc2.units.SimpleUnit;

import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.*;
import ucar.unidata.util.StringUtil;

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

/**
 * AWIPS netcdf output.
 *
 * @author caron
 * @version $Revision: 1.11 $ $Date: 2004/12/09 00:17:31 $
 */

public class AWIPSConvention extends CoordSysBuilder {

  /** return true if we think this is a AWIPS file. */
  public static boolean isMine( NetcdfFile ncfile) {
    return (null != ncfile.findGlobalAttribute("projName")) &&
       (null != ncfile.findDimension("charsPerLevel"));
  }

  private final boolean debugProj = false;
  private final boolean debugBreakup = false;

  private ArrayList mungedList = new ArrayList();
  private ProjectionCT projCT = null;
  private double startx, starty;

  protected void augmentDataset( NetcdfDataset ds, CancelTask cancelTask) {
    this.conventionName = "AWIPS";

    String projName = ds.findAttValueIgnoreCase(null, "projName", "none");
    if (projName.equalsIgnoreCase("LAMBERT_CONFORMAL"))
      projCT = makeLCProjection(ds, projName);

    Dimension dimx = ds.findDimension("x");
    int nx = dimx.getLength();

    Dimension dimy = ds.findDimension("y");
    int ny = dimy.getLength();

    ds.addCoordinateAxis( makeXCoordAxis( ds, nx, "x"));
    ds.addCoordinateAxis( makeYCoordAxis( ds, ny, "y"));

    CoordinateAxis timeCoord = makeTimeCoordAxis( ds);
    if (timeCoord != null) {
      ds.addCoordinateAxis( timeCoord);
      Dimension d =  timeCoord.getDimension(0);
      if (!d.getName().equals( timeCoord.getShortName()) )
        timeCoord.addAttribute( new Attribute("_CoordinateAliasForDimension", d.getName()));
    }

    // AWIPS cleverly combines multiple z levels into a single variable (!!)
    Iterator vars = ds.getVariables().iterator();
    while (vars.hasNext()) {
      Variable ncvar = (Variable) vars.next();
      String levelName = ncvar.getName()+"Levels";
      Variable levelVar = (Variable) ds.findVariable(levelName);
      if (levelVar == null) continue;
      if (levelVar.getRank() != 2) continue;
      if (levelVar.getDataType() != DataType.CHAR) continue;

      ArrayList levels = breakupLevels( ds, levelVar);
      try {
        createNewVariables(ds, ncvar, levels, levelVar.getDimension(0));
      }
      catch (InvalidRangeException ex) {
        parseInfo.append("createNewVariables InvalidRangeException\n");
      }
      mungedList.add( ncvar);
    }

    if (projCT != null) {
        VariableDS v = makeCoordinateTransformVariable(ds, projCT);
        v.addAttribute( new Attribute("_CoordinateAxes", "x y"));
        ds.addVariable(null, v);
    }


    ds.finish();

      // kludge in fixing the units
    List vlist = ds.getVariables();
    for (int i=0; i<vlist.size(); i++) {
      Variable v = (Variable) vlist.get(i);
      Attribute att = v.findAttributeIgnoreCase( "units");
      if (att != null) {
        String units = att.getStringValue();
        v.addAttribute( new Attribute( "units", normalize( units))); // removes the old
      }
    }

  }

  // pretty much WRF specific
  private String normalize( String units) {
    if (units.equals("/second")) units="1/sec";
    if (units.equals("degrees K")) units="K";
    else {
      units = StringUtil.substitute( units, "**", "^");
      units = StringUtil.remove( units, ')');
      units = StringUtil.remove( units, '(');
    }
    return units;
  }

  // take a combined level variable and create multiple levels out of it
  // return the list of Dimensions that were created
  private ArrayList breakupLevels( NetcdfDataset ds, Variable levelVar) {
    if (debugBreakup)  parseInfo.append("breakupLevels = "+levelVar.getName()+"\n");
    ArrayList dimList = new ArrayList();

    ArrayChar levelVarData;
    try {
      levelVarData = (ArrayChar) levelVar.read();
    } catch (IOException ioe) {
      return dimList;
    }

    ArrayList values = null;
    String currentUnits = null;
    ArrayChar.StringIterator iter = levelVarData.getStringIterator();
    while (iter.hasNext()) {
      String s = iter.next();
      if (debugBreakup)  parseInfo.append("   "+s+"\n");
      StringTokenizer stoke = new StringTokenizer(s);

      // first token is the unit
      String units = stoke.nextToken().trim();
      if (!units.equals(currentUnits)) {
        if (values != null)
          dimList.add( makeZCoordAxis(ds, values, currentUnits));
        values = new ArrayList();
        currentUnits = units;
      }

      // next token is the value
      if (stoke.hasMoreTokens())
        values.add(stoke.nextToken());
      else
        values.add("0");
    }
    if (values != null)
      dimList.add(makeZCoordAxis(ds, values, currentUnits));

    if (debugBreakup) parseInfo.append("  done breakup"+"\n");

    return dimList;
  }

  // make a new variable out of the list in "values"
  private Dimension makeZCoordAxis( NetcdfDataset ds, ArrayList values, String units) {
    int len = values.size();
    String name = makeZCoordName( units);
    if (len > 1)
      name = name + Integer.toString(len);
    else
      name = name + values.get(0);
    StringUtil.replace(  name, ' ', "-");

    // LOOK replace with check against actual values !!!
    Dimension dim;
    if (null != (dim = (Dimension) ds.getRootGroup().findDimension(name))) {
      if (dim.getLength() == len) {
        if (debugBreakup) parseInfo.append("  use existing dim"+dim);
        return dim;
      }
    }

    // create new one
    dim = new Dimension(name, len, true);
    ds.addDimension( null, dim);
    if (debugBreakup)  parseInfo.append("  make Dimension = "+name+ " length = "+len+"\n");

    // if (len < 2) return dim; // skip 1D

    if (debugBreakup)  parseInfo.append("  make ZCoordAxis = "+name+ " length = "+len+"\n");

    CoordinateAxis v = new CoordinateAxis1D( ds, null, name, DataType.DOUBLE, name,
       makeUnitsName( units), makeLongName(name));
    String positive = getZisPositive( ds, v);
    if (null != positive)
      v.addAttribute( new Attribute("_CoordinateZIsPositive", positive));

    ds.setValues( v, values);
    ds.addCoordinateAxis(v);

    parseInfo.append("Created Z Coordinate Axis = ");
    v.getNameAndDimensions(parseInfo, true, false);
    parseInfo.append("\n");

    return dim;
  }

  private String makeZCoordName(String units) {
    if (units.equalsIgnoreCase("MB")) return "PressureLevels";
    if (units.equalsIgnoreCase("K")) return "PotTempLevels";
    if (units.equalsIgnoreCase("BL")) return "BoundaryLayers";

    if (units.equalsIgnoreCase("FHAG")) return "FixedHeightAboveGround";
    if (units.equalsIgnoreCase("SFC")) return "Surface";
    if (units.equalsIgnoreCase("MSL")) return "MeanSeaLevel";
    if (units.equalsIgnoreCase("FRZ")) return "FreezingLevel";
    if (units.equalsIgnoreCase("TROP")) return "Tropopause";
    if (units.equalsIgnoreCase("MAXW")) return "MaxWindLevel";
    return units;
  }

  private String makeUnitsName(String units) {
    if (units.equalsIgnoreCase("MB")) return "hPa";
    if (units.equalsIgnoreCase("BL")) return "hPa";
    if (units.equalsIgnoreCase("FHAG")) return "m";
    return "";
  }

  private String makeLongName(String name) {
    if (name.equalsIgnoreCase("PotTempLevels")) return "Potential Temperature Level";
    if (name.equalsIgnoreCase("BoundaryLayers")) return "BoundaryLayer hectoPascals above ground";
    else return name;
  }

  // create new variables as sections of ncVar
  private void createNewVariables( NetcdfDataset ds, Variable ncVar, ArrayList newDims,
     Dimension levelDim) throws InvalidRangeException {

    List dims = ncVar.getDimensions();
    int newDimIndex = dims.indexOf(levelDim);
    //String shapeS = ncVar.getShapeS();

    int[] origin = new int[ncVar.getRank()];
    int[] shape = ncVar.getShape();
    int count = 0;
    for (int i=0; i<newDims.size(); i++) {
      Dimension dim = (Dimension) newDims.get(i);
      String name = ncVar.getName()+"-"+dim.getName();

      origin[newDimIndex] = count;
      shape[newDimIndex] = dim.getLength();

      List ranges = Range.factory( origin, shape);
      Variable varNew = ncVar.section( ranges);
      varNew.setName( name);
      varNew.setDimension( newDimIndex, dim);

      // synthesize long name
      String long_name = ds.findAttValueIgnoreCase(ncVar, "long_name", ncVar.getName());
      long_name = long_name+"-"+dim.getName();
      ds.addVariableAttribute( varNew, new Attribute("long_name", long_name));

      ds.addVariable( null, varNew);

      parseInfo.append("Created New Variable as section = ");
      varNew.getNameAndDimensions(parseInfo, true, false);
      parseInfo.append("\n");

      count += dim.getLength();
    }
  }


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


  protected AxisType getAxisType( NetcdfDataset ds, VariableEnhanced ve) {
    Variable v = (Variable) ve;
    String vname = v.getName();

   if (vname.equalsIgnoreCase("x"))
      return AxisType.GeoX;

    if (vname.equalsIgnoreCase("lon"))
      return AxisType.Lon;

    if (vname.equalsIgnoreCase("y"))
      return AxisType.GeoY;

    if (vname.equalsIgnoreCase("lat"))
      return AxisType.Lat;

    if (vname.equalsIgnoreCase("record"))
      return AxisType.Time;
    Dimension dim = v.getDimension(0);
    if ((dim != null) && dim.getName().equalsIgnoreCase("record"))
      return AxisType.Time;

    String unit = ve.getUnitsString();
    if (unit != null) {
      if ( SimpleUnit.isCompatible("millibar", unit))
        return AxisType.Pressure;

      if ( SimpleUnit.isCompatible("m", unit))
        return AxisType.Height;
    }


    return AxisType.GeoZ;
  }

  protected void makeCoordinateTransforms( NetcdfDataset ds) {
    if (projCT != null) {
      VarProcess vp = findVarProcess(projCT.getName());
      vp.isCoordinateTransform = true;
      vp.ct = projCT;
    }
    super.makeCoordinateTransforms( ds);
  }

  private String getZisPositive( NetcdfDataset ds, CoordinateAxis v) {

    String attValue = ds.findAttValueIgnoreCase(v, "positive", null);
    if (null != attValue)
      return attValue.equalsIgnoreCase("up") ? "up" : "down";

    String unit = v.getUnitsString();
    if ((unit != null) && SimpleUnit.isCompatible("millibar", unit))
      return "down";
    if ((unit != null) && SimpleUnit.isCompatible("m", unit))
      return "up";

      // dunno
    return null;
  }

  private ProjectionCT makeLCProjection(NetcdfDataset ds, String name) throws NoSuchElementException {
    double centralLat = findAttributeDouble( ds, "centralLat");
    double centralLon = findAttributeDouble( ds, "centralLon");
    double rotation = findAttributeDouble( ds, "rotation");

    // we have to project in order to find the origin
    LambertConformal lc = new LambertConformal(rotation, centralLon, centralLat, centralLat);
    double lat0 = findAttributeDouble( ds, "lat00");
    double lon0 = findAttributeDouble( ds, "lon00");
    ProjectionPointImpl start = (ProjectionPointImpl) lc.latLonToProj( new LatLonPointImpl( lat0, lon0));
    if (debugProj)  parseInfo.append("getLCProjection start at proj coord "+start+"\n");
    startx = start.getX();
    starty = start.getY();

    ProjectionCT ct = new ProjectionCT(name, "FGDC", lc);

    return ct;
  }

  private CoordinateAxis makeXCoordAxis(NetcdfDataset ds, int nx, String xname) {
    double dx = findAttributeDouble(ds, "dxKm");
    CoordinateAxis v = new CoordinateAxis1D(ds, null, xname, DataType.DOUBLE, xname, "km", "x on projection");
    ds.setValues(v, nx, startx, dx);

    parseInfo.append("Created X Coordinate Axis = ");
    v.getNameAndDimensions(parseInfo, true, false);
    parseInfo.append("\n");

    return v;
  }

  private CoordinateAxis makeYCoordAxis( NetcdfDataset ds, int ny, String yname) {
    double dy = findAttributeDouble( ds, "dyKm");
    CoordinateAxis v = new CoordinateAxis1D( ds, null, yname, DataType.DOUBLE, yname, "km", "y on projection");
    ds.setValues( v, ny, starty, dy);

    parseInfo.append("Created Y Coordinate Axis = ");
    v.getNameAndDimensions(parseInfo, true, false);
    parseInfo.append("\n");

    return v;
  }

  private CoordinateAxis makeTimeCoordAxis( NetcdfDataset ds) {
    Variable timeVar = (Variable) ds.findVariable("valtimeMINUSreftime");
    Dimension recordDim = ds.findDimension("record");
    Array vals = null;

    try {
      vals = timeVar.read();
    } catch (IOException ioe) {
      return null;
    }

    // it seems that the record dimension does not always match valtimeMINUSreftime dimension!!
    // HAHAHAHAHAHAHAHA !
    int recLen = recordDim.getLength();
    int valLen = (int) vals.getSize();
    if (recLen != valLen) {
      try {
        vals = vals.sectionNoReduce(new int[] {0}, new int[] {recordDim.getLength()}, null);
        parseInfo.append(" corrected the TimeCoordAxis length\n");
      } catch (InvalidRangeException e) {
        parseInfo.append("makeTimeCoordAxis InvalidRangeException\n");
      }
    }

    // create the units out of the filename if possible
    String units = makeTimeUnitFromFilename(ds.getLocation());
    if (units == null) // ok that didnt work, try something else
      return makeTimeCoordAxisFromReference(ds, timeVar, vals);

    // create the coord axis
    String desc = "synthesized time coordinate from valtimeMINUSreftime and filename YYYYMMDD_HHMM";
    CoordinateAxis1D timeCoord = new CoordinateAxis1D( ds, null, "timeCoord", DataType.INT, "record", units, desc);
    timeCoord.addAttribute(new Attribute("long_name", desc));

    timeCoord.setCachedData( vals, true);

    parseInfo.append("Created Time Coordinate Axis = ");
    timeCoord.getNameAndDimensions(parseInfo, true, false);
    parseInfo.append("\n");

    return timeCoord;
  }

  private String makeTimeUnitFromFilename( String dsName) {
    dsName = dsName.replace('\\','/');

    // posFirst: last '/' if it exists
    int posFirst = dsName.lastIndexOf('/');
    if (posFirst < 0) posFirst = 0;

     // posLast: next '.' if it exists
     int posLast = dsName.indexOf(".", posFirst);
     if (posLast < 0)
       dsName = dsName.substring(posFirst+1);
    else
       dsName = dsName.substring(posFirst+1, posLast);

    // gotta be YYYYMMDD_HHMM
    if (dsName.length() != 13)
      return null;

    String year = dsName.substring(0,4);
    String mon = dsName.substring(4,6);
    String day = dsName.substring(6,8);
    String hour = dsName.substring(9,11);
    String min = dsName.substring(11,13);

    return "seconds since "+year+"-"+mon+"-"+day+" "+hour+":"+min+":0";
  }

  // construct time coordinate from reftime variable
  private CoordinateAxis makeTimeCoordAxisFromReference( NetcdfDataset ds, Variable timeVar, Array vals) {
    Variable refVar = (Variable) ds.findVariable("reftime");
    if (refVar == null) return null;
    double refValue = 0.0;
    try {
      Array refArray = refVar.read();
      refValue = refArray.getDouble(refArray.getIndex()); // get the first value
    } catch (IOException ioe) {
      return null;
    }
    // construct the values array - make it a double to be safe
    Array dvals = Array.factory(double.class, vals.getShape());
    IndexIterator diter = dvals.getIndexIterator();
    IndexIterator iiter = vals.getIndexIterator();
    while (iiter.hasNext())
      diter.setDoubleNext( iiter.getDoubleNext() + refValue); // add reftime to each of the values

    String units = ds.findAttValueIgnoreCase(refVar, "units", "seconds since 1970-1-1 00:00:00");
    units = normalize(units);
    String desc = "synthesized time coordinate from reftime, valtimeMINUSreftime";
    CoordinateAxis1D timeCoord = new CoordinateAxis1D( ds, null, "timeCoord", DataType.DOUBLE, "record", units, desc);
    timeCoord.addAttribute(new Attribute("long_name", desc));

    timeCoord.setCachedData( dvals, true);

    parseInfo.append("Created Time Coordinate Axis From Reference = ");
    timeCoord.getNameAndDimensions(parseInfo, true, false);
    parseInfo.append("\n");

    return timeCoord;
  }

  private double findAttributeDouble( NetcdfDataset ds, String attname) {
    Attribute att = ds.findGlobalAttributeIgnoreCase(attname);
    return att.getNumericValue().doubleValue();
  }


}

/**
 * $Log: AWIPSConvention.java,v $
 * Revision 1.11  2004/12/09 00:17:31  caron
 * *** empty log message ***
 *
 * Revision 1.10  2004/12/08 18:08:31  caron
 * implement _CoordinateAliasForDimension
 *
 * Revision 1.9  2004/12/07 02:43:21  caron
 * *** empty log message ***
 *
 * Revision 1.8  2004/12/07 01:29:28  caron
 * redo convention parsing, use _Coordinate encoding.
 *
 * Revision 1.7  2004/12/03 04:46:24  caron
 * no message
 *
 * Revision 1.6  2004/12/01 05:53:39  caron
 * ncml pass 2, new convention parsing
 *
 * Revision 1.5  2004/10/29 00:14:09  caron
 * no message
 *
 * Revision 1.4  2004/10/19 19:45:02  caron
 * misc
 *
 * Revision 1.3  2004/09/22 18:43:03  caron
 * move common to ucar.unidata; projections use Parameter, no tAttribute
 *
 * Revision 1.2  2004/09/09 22:47:40  caron
 * station updates
 *
 * Revision 1.1  2004/08/16 20:53:49  caron
 * 2.2 alpha (2)
 *
 * Revision 1.7  2004/06/07 20:24:52  caron
 * move SimpleUnit to ucar.unit.units, add aliases in UnitDB
 *
 * Revision 1.6  2003/07/12 22:04:31  caron
 * minor
 *
 * Revision 1.5  2003/05/01 23:37:07  caron
 * fix latest AWIPS time fiasco
 *
 * Revision 1.4  2003/04/25 01:32:43  caron
 * fix AWIPS time coord
 *
 * Revision 1.3  2003/04/21 21:23:27  caron
 * fix AWIPS coordinate variables with same units
 *
 * Revision 1.2  2003/04/18 23:03:57  caron
 * make long names correct
 *
 * Revision 1.1  2003/04/08 15:06:26  caron
 * nc2 version 2.1
 *
 * Revision 1.1.1.1  2002/02/26 17:24:43  caron
 * import sources
 *
 * Revision 1.11  2001/10/26 16:37:50  caron
 * catch Exception when no grid_type
 *
 * Revision 1.10  2001/10/26 16:34:13  caron
 * extra semicolon
 *
 * Revision 1.9  2001/03/09 19:33:21  caron
 * minor
 *
 * Revision 1.8  2001/02/06 22:44:07  caron
 * new VMD Strategy
 */
