// $Id: CF1Convention.java,v 1.12 2004/12/07 02:43:21 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.units.SimpleUnit;
import ucar.nc2.dataset.*;
import ucar.unidata.geoloc.vertical.*;

import java.io.*;
import java.util.*;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.util.Parameter;

/**
 * CF-1 Convention.
 * see http://www.cgd.ucar.edu/cms/eaton/cf-metadata/index.html
 *
 * <i>
 * "The CF conventions for climate and forecast metadata are designed to promote the
 * processing and sharing of files created with the netCDF API. The conventions define
 * metadata that provide a definitive description of what the data in each variable
 * represents, and of the spatial and temporal properties of the data.
 * This enables users of data from different sources to decide which quantities are
 * comparable, and facilitates building applications with powerful extraction, regridding,
 * and display capabilities."
 * </i>
 *
 * @author caron
 * @version $Revision: 1.12 $ $Date: 2004/12/07 02:43:21 $
 */

public class CF1Convention extends CSMConvention {

  private static String[] vertical_coords = {"atmosphere_sigma_coordinate",
                                             "atmosphere_hybrid_sigma_pressure_coordinate",
                                             "atmosphere_hybrid_height_coordinate",
                                             "atmosphere_sleve_coordinate",
                                             "ocean_sigma_coordinate",
                                             "ocean_s_coordinate",
                                             "ocean_sigma_z_coordinate",
                                             "ocean_double_sigma_coordinate"};

  public void augmentDataset( NetcdfDataset ds, CancelTask cancelTask) {
    this.conventionName = "CF-1.0";

    // look for transforms
    List vars = ds.getVariables();
    for (int i = 0; i < vars.size(); i++) {
      Variable v = (Variable) vars.get(i);

      // look for vertical transforms
      String sname = ds.findAttValueIgnoreCase(v, "standard_name", null);
      if (sname != null) {
        for (int j=0; j<vertical_coords.length; j++)
          if (sname.equalsIgnoreCase(vertical_coords[j])) {
            v.addAttribute( new Attribute("_CoordinateTransformType", TransformType.Vertical.toString()));
            v.addAttribute( new Attribute("_CoordinateAxes", v.getName()));
          }
      }

      // look for horiz transforms
      String grid_mapping_name = ds.findAttValueIgnoreCase(v, "grid_mapping_name", null);
      if (grid_mapping_name != null) {
          v.addAttribute( new Attribute("_CoordinateTransformType", TransformType.Projection.toString()));
          v.addAttribute( new Attribute("_CoordinateAxes", "GeoX GeoZ"));
      }

    }

  }

  // we assume that coordinate axes get identified by
  //  1) being coordinate variables or
  //  2) being listed in coordinates attribute.

  /** Augment CSM axis type identification with "projection_x_coordinate", "projection_y_coordinate"
   *  and  the various dimensionless vertical coordinates */
  protected AxisType getAxisType( NetcdfDataset ncDataset, VariableEnhanced v) {

    String sname = ncDataset.findAttValueIgnoreCase((Variable) v, "standard_name", null);
    if (sname != null) {

      if (sname.equalsIgnoreCase("projection_x_coordinate"))
        return AxisType.GeoX;

      if (sname.equalsIgnoreCase("projection_y_coordinate"))
        return AxisType.GeoY;

      for (int i=0; i<vertical_coords.length; i++)
        if (sname.equalsIgnoreCase(vertical_coords[i]))
          return AxisType.GeoZ;
    }

    // dont use axis attribute - not clear enough

    return super.getAxisType(ncDataset, v);
  }

}

  /* protected void constructCoordinateTransforms(NetcdfDataset ds) {
    parseInfo.append("Creating coordinate transforms:\n");

   // loop over Variables, looking for grid_mapping attribute
    Iterator iter = ds.getVariables().iterator();
    while (iter.hasNext()) {
      Variable v = (Variable) iter.next();
      String grid_mapping = ds.findAttValueIgnoreCase( v, "grid_mapping", null);
      if (grid_mapping == null) continue;
      CoordinateTransform ct = (CoordinateTransform) ctHash.get(grid_mapping);
      if (ct != null) continue;

      // found it - must be name of Variable with grid_mapping_name attribute
      // LOOK in the same group !!
      Variable map = v.getParentGroup().findVariable(grid_mapping);
      if (map == null) {
        parseInfo.append(" cant find grid_mapping "+grid_mapping+" variable\n");
        continue;
      }
      String grid_mapping_name = ds.findAttValueIgnoreCase( map, "grid_mapping_name", null);
      if (grid_mapping_name == null) {
        parseInfo.append(" grid_mapping variable "+map.getName()+" must have grid_mapping_name attribute\n");
        continue;
      }

      // figure out which grid_mapping this is
      if (grid_mapping_name.equals("lambert_conformal_conic")) {
        ct = makeLCProjection(map, grid_mapping_name);
      } else {
        parseInfo.append(" grid_mapping variable "+map+
                         "\n has unknown/unimplemented grid_mapping_name = "+grid_mapping_name);
        continue;
      }
      if (ct != null)
        ctHash.put( "projection", ct); // look wrong
    }

  }

  private CoordinateTransform makeLCProjection(Variable mapv, String gridmap_name) {
    Attribute a;
    if (null == (a = findAttribute( mapv, gridmap_name, "standard_parallel"))) return null;
    double par1 = a.getNumericValue(0).doubleValue();
    double par2 = (a.getLength() > 1) ? a.getNumericValue(1).doubleValue() : par1;

    if (null == (a = findAttribute( mapv, gridmap_name, "longitude_of_central_meridian"))) return null;
    double lon0 = a.getNumericValue(0).doubleValue();

    if (null == (a = findAttribute( mapv, gridmap_name, "latitude_of_projection_origin"))) return null;
    double lat0 = a.getNumericValue(0).doubleValue();

    LambertConformal lc = new LambertConformal(lat0, lon0, par1, par2);
    CoordinateTransform rs = new ProjectionCT(gridmap_name, "FGDC", lc);

    parseInfo.append(" Made Lambert Conformal projection:\n");
    return rs;
  }

  private Attribute findAttribute( Variable v, String gridmap_name, String attName) {
    Attribute a = v.findAttribute(attName);
    if (a == null)
      parseInfo.append(" grid_mapping for "+gridmap_name+" variable = " + v +
                       "\n  must have attribute "+attName);
    return a;
  }

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

  protected boolean constructVerticalTransform(CoordinateAxis v) {

    String sname = ncDataset.findAttValueIgnoreCase(v, "standard_name", null);
    if (null == sname) return false;

    String formula = ncDataset.findAttValueIgnoreCase(v, "formula_terms", null);
    if (null == formula) return false;

    if (sname.equalsIgnoreCase("atmosphere_sigma_coordinate")) {
      try {
        SigmaFormula f = new SigmaFormula(formula);
        CoordinateTransform ct = f.makeCT( v.getName(), ncDataset);
        ctHash.put( v.getName(), ct);
        parseInfo.append("  made Sigma CoordinateTransform for "+v.getName()+
                        " formula= "+formula+"\n");
        showFormula( formula);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make Sigma CoordinateTransform for "+v.getName()+"\n");
      }
      return true;
    }

    if (sname.equalsIgnoreCase("atmosphere_hybrid_sigma_pressure_coordinate")) {
      HybridSigmaPressureFormula f = new HybridSigmaPressureFormula(formula);
      try {
        CoordinateTransform ct = f.makeCT( v.getName(), ncDataset);
        ctHash.put( v.getName(), ct);
        parseInfo.append("  made HybridSigmaPressure CoordinateTransform for "+v.getName()+
            " formula= "+formula+"\n");
        showFormula( formula);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make HybridSigmaPressure CoordinateTransform for "+v.getName()+"\n");
      }
      return true;
    }

   if (sname.equalsIgnoreCase("ocean_s_coordinate")) {
      OceanSFormula f = new OceanSFormula(formula);
      try {
        CoordinateTransform ct = f.makeCT( v.getName(), ncDataset);
        ctHash.put( v.getName(), ct);
        parseInfo.append("  made OceanS CoordinateTransform for "+v.getName()+
            " formula= "+formula+"\n");;
        showFormula( formula);
      } catch (IOException ioe) {
        parseInfo.append("  failed to make OceanS CoordinateTransform for "+v.getName()+"\n");
      }
      return true;
    }

    return false;
  }

  private void showFormula( String formula) {
    StringTokenizer stoke = new StringTokenizer( formula);

    while (stoke.hasMoreTokens()) {
      String name = stoke.nextToken();
      String vname = stoke.nextToken();
      Variable v = ncDataset.findVariable( vname);

      if (v == null) {
        parseInfo.append(" ***ERROR Cant find   "+vname+" in formula\n");
        return;
      }

      parseInfo.append("    "+name+" = ");
      v.getNameAndDimensions(parseInfo, false, false);
      parseInfo.append("\n");
    }
  }

  private double[] makeDataValues(NetcdfFile ds, String varName) throws IOException {
    Variable dataVar = ds.findVariable(varName);
    Array data = dataVar.read();
    double d[] = new double[ (int) data.getSize()];
    int count = 0;
    IndexIterator ii = data.getIndexIterator();
    while (ii.hasNext())
      d[count++] = ii.getDoubleNext();
    return d;
  }

  private class SigmaFormula {
    String sigma="", ps="", ptop="";

    SigmaFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("sigma:"))
          sigma = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ps:"))
          ps = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ptop:"))
          ptop = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds) throws IOException {
      CoordinateTransform rs = new VerticalCT("sigma-"+varName, conventionName, VerticalCT.Type.Sigma);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = ptop + sigma(z)*(surfacePressure(x,y)-ptop)"));
      rs.addParameter(new Parameter("surfacePressure variable name", ps));
      rs.addParameter(new Parameter("sigma", makeDataValues(ds, sigma)));
      rs.addParameter(new Parameter("ptop", makeDataValues(ds, ptop)));
      return rs;
    }

    public String toString() { return "Sigma "+sigma + ps + ptop; }
  }

 private class HybridSigmaPressureFormula {
    String a="", b="", ps="", p0="";

    HybridSigmaPressureFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("a:"))
          a = stoke.nextToken();
        else if (toke.equalsIgnoreCase("b:"))
          b = stoke.nextToken();
        else if (toke.equalsIgnoreCase("ps:"))
          ps = stoke.nextToken();
        else if (toke.equalsIgnoreCase("p0:"))
          p0 = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds)  throws IOException {
      CoordinateTransform rs = new VerticalCT("hybrid_sigma_pressure-"+varName, conventionName, VerticalCT.Type.HybridSigmaPressure);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = a(z)*p0 + b(z)*surfacePressure(x,y)"));
      rs.addParameter(new Parameter("surfacePressure variable name", ps));
      rs.addParameter(new Parameter("a variable name", a));
      rs.addParameter(new Parameter("b variable name", b));
      rs.addParameter(new Parameter("p0", makeDataValues(ds, p0)));
      return rs;
    }

    public String toString() { return "HybridSigma "+a + b + ps + p0; }
  }

  // :formula_terms = "s: s_rho eta: zeta depth: h a: theta_s b: theta_b depth_c: hc";
  private class OceanSFormula {
    String s="", eta="", depth="", a="", b="", depth_c="";

    OceanSFormula( String formula) {
       // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();
        if (toke.equalsIgnoreCase("s:"))
          s = stoke.nextToken();
        else if (toke.equalsIgnoreCase("eta:"))
          eta = stoke.nextToken();
        else if (toke.equalsIgnoreCase("depth:"))
          depth = stoke.nextToken();
        else if (toke.equalsIgnoreCase("a:"))
          a = stoke.nextToken();
        else if (toke.equalsIgnoreCase("b:"))
          b = stoke.nextToken();
        else if (toke.equalsIgnoreCase("depth_c:"))
          depth_c = stoke.nextToken();
      }
    }

    CoordinateTransform makeCT( String varName, NetcdfFile ds)  throws IOException {

      CoordinateTransform rs = new VerticalCT("oceanS-"+varName, conventionName, VerticalCT.Type.OceanS);
      rs.addParameter((new Parameter("height formula", "height(x,y,z) = eta(x,y)*(1+s(z)) + depth_c*s(z) + (depth(x,y)-depth_c)*C(z)")));
      rs.addParameter((new Parameter("C formula", "C(z) = (1-b)*sinh(a*s(z))/sinh(a) + b*(tanh(a*(s(z)+0.5))/(2*tanh(0.5*a))-0.5)")));
      rs.addParameter((new Parameter(OceanS.ETA, eta)));
      rs.addParameter((new Parameter(OceanS.S, s)));
      rs.addParameter((new Parameter(OceanS.DEPTH, depth)));
      rs.addParameter((new Parameter(OceanS.DEPTH_C, makeDataValues(ds, depth_c))));
      rs.addParameter((new Parameter(OceanS.A, makeDataValues(ds, a))));
      rs.addParameter((new Parameter(OceanS.B, makeDataValues(ds, b))));

      return rs;
    }


    public String toString() { return "OceanS "+s + eta + depth + a + b + depth_c; }
  }

}

/*  protected void constructCoordAxes(NetcdfDataset ds) {
    // do coordinate Variables
    findCoordinateVariables(ds);
    assignAxisType(ds); // want ordering on dimensionless coordaintes

  /* "atmosphere_sigma_coordinate: vertical coordinate"
"atmosphere_hybrid_sigma_pressure_coordinate: vertical coordinate"
"atmosphere_hybrid_height_coordinate: vertical coordinate"
"ocean_sigma_coordinate: vertical coordinate"
"ocean_s_coordinate: vertical coordinate"

      // look for dimensionless coordinates
    Iterator vars = ds.getVariables().iterator();
    while (vars.hasNext()) {
      Variable v = (Variable) vars.next();
      String standardName = ds.findAttValueIgnoreCase(v, "standard_name", null);
      String formula = ds.findAttValueIgnoreCase(v, "formula_terms", null);
      if ((null == standardName) || (formula == null))
        continue;

      if (standardName.equalsIgnoreCase("atmosphere_hybrid_sigma_pressure_coordinate")) {
        String longName = ds.findAttValueIgnoreCase(v, "long_name", null);
        String name = "pressure_" + v.getName();
        CoordinateAxisHybrid ca = new CoordinateAxisHybrid(ds, name, longName, formula);
        if (ca.ok)
          ds.addCoordinateAxis( ca);
      }
     } // loop over vars
  }


  private class CoordinateAxisHybrid extends ucar.nc2.dataset.CoordinateAxis {
    private String units;
    boolean ok = true;
    private TreeSet dims = new TreeSet(new DimAxisComparator());

    CoordinateAxisHybrid(NetcdfDataset ds, String name, String longName, String formula) {
      super( ds, name);
      DataType dataType = DataType.getType("double");
      this.elementType = dataType.getPrimitiveClassType();

      // parse the formula string
      StringTokenizer stoke = new StringTokenizer(formula);
      while (stoke.hasMoreTokens()) {
        String toke = stoke.nextToken();

        if (toke.equalsIgnoreCase("a:")) {
          Variable var = addVariable(ds, stoke.nextToken());
          if (var != null)
            makeAxis( ds, var);
          else
            ok = false;

        } else if (toke.equalsIgnoreCase("b:")) {
          Variable var = addVariable(ds, stoke.nextToken());
          if (var != null)
            makeAxis( ds, var);
          else
            ok = false;

        } else if (toke.equalsIgnoreCase("p0:")) {
          Variable var = addVariable(ds, stoke.nextToken());
          if (var != null) {
            if (units == null)
              units = getUnitsString(var);
          } else
            ok = false;

        } else if (toke.equalsIgnoreCase("ps:")) {
          Variable var = addVariable(ds, stoke.nextToken());
          if (var != null) {
            if (units == null)
              units = getUnitsString(var);
          } else
            ok = false;

         }
      }

      StringBuffer buff = new StringBuffer();
      Iterator dimIter = dims.iterator();
      while (dimIter.hasNext()) {
        buff.append(((DimAxis)dimIter.next()).getName());
        buff.append(" ");
      }

      this.shapeS = buff.toString();
      finish();

      if (units != null)
        addAttribute(new Attribute("units", units));
      addAttribute(new Attribute("long_name", longName));
   }

    private boolean makeAxis( NetcdfDataset ds, Variable ncvar) {
      if (!(ncvar instanceof CoordinateAxis)) {
        CoordinateAxis axis = ds.addCoordinateAxis( ncvar);
        axis.setIsAuxilary( true);
        axis.setAxisType( AxisType.GeoZ);
      }
      return true;
    }

    private Variable addVariable( NetcdfDataset ds, String varName) {
      Variable ncvar = (Variable) ds.findVariable( varName);
      if (ncvar == null) {
        System.out.println("CoordinateHybrid cant find variable "+ varName);
        return null;
      }
      // track all the dimensions used
      Iterator dimIter = ncvar.getDimensions().iterator();
      while (dimIter.hasNext())
        dims.add(new DimAxis( ds, (Dimension) dimIter.next()));
      return ncvar;
    }
  }

  private class DimAxis {
    Dimension dim;
    CoordinateAxis axis;
    DimAxis( NetcdfDataset ds, Dimension dim) {
      this.dim = dim;
      this.axis = findCoordAxis( ds, dim.getName());
    }
    String getName() { return dim.getName(); }
  }

  private class DimAxisComparator implements java.util.Comparator {
    public int compare(Object o1, Object o2) {
      DimAxis d1 = (DimAxis) o1;
      DimAxis d2 = (DimAxis) o2;

      if ((d1.axis == null) || (d2.axis == null))
        return d1.getName().compareTo( d2.getName());

      AxisType t1 = d1.axis.getAxisType();
      AxisType t2 = d2.axis.getAxisType();

      if ((t1 == null) && (t2 == null))
        return d1.getName().compareTo( d2.getName());
      if (t1 == null)
        return -1;
      if (t2 == null)
        return 1;

      return t1.compareTo( t2);
    }
    public boolean equals(Object obj) { return (this == obj); }
  } */


/**
 * $Log: CF1Convention.java,v $
 * Revision 1.12  2004/12/07 02:43:21  caron
 * *** empty log message ***
 *
 * Revision 1.11  2004/12/07 01:29:28  caron
 * redo convention parsing, use _Coordinate encoding.
 *
 * Revision 1.10  2004/12/03 04:46:24  caron
 * no message
 *
 * Revision 1.9  2004/12/01 05:53:39  caron
 * ncml pass 2, new convention parsing
 *
 * Revision 1.8  2004/10/29 00:14:09  caron
 * no message
 *
 * Revision 1.7  2004/10/23 21:36:10  caron
 * no message
 *
 * Revision 1.6  2004/10/22 00:52:04  caron
 * javadoc
 *
 * Revision 1.5  2004/09/30 00:33:41  caron
 * *** empty log message ***
 *
 * Revision 1.4  2004/09/22 21:26:42  caron
 * ucar.unidata.util.Parameter
 *
 * 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/22 13:46:37  caron
 * *** empty log message ***
 *
 * Revision 1.1  2004/08/16 20:53:49  caron
 * 2.2 alpha (2)
 *
 * Revision 1.4  2003/10/02 20:33:49  caron
 * move SimpleUnit to dataset; add <units> tag; add projections to CF
 *
 * Revision 1.3  2003/07/12 23:30:08  caron
 * fix javadoc
 *
 * Revision 1.2  2003/07/12 22:09:04  caron
 * add vertical transformations
 *
 * 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
 */