// $Id: NcMLWriter.java,v 1.5 2004/12/08 18:08:32 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.ncml;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.Attribute;
import ucar.nc2.dataset.*;
import ucar.unidata.util.StringUtil;

import org.jdom.*;
import org.jdom.output.XMLOutputter;

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

/**
 * Helper class to write ncml.
 *
 * @see ucar.nc2.NetcdfFile
 * @author caron
 * @version $Revision: 1.5 $ $Date: 2004/12/08 18:08:32 $
 */

public class NcMLWriter {
  protected static final Namespace ncNS = Namespace.getNamespace(XMLEntityResolver.NJ22_NAMESPACE);
  protected static final Namespace xsiNS = Namespace.getNamespace("xsi", XMLEntityResolver.W3C_XML_NAMESPACE);

  private NetcdfDataset ncd;
  private XMLOutputter fmt;

  /**
   * Write a NetcdfFile as an XML document to the specified stream.
   *
   * @param ncfile NcML for this NetcdfFile
   * @param os write to this OutputStream
   * @param location NcML location
   * @throws IOException
   */
  public void writeXML(NetcdfFile ncfile, OutputStream os, String location) throws IOException {

    if (ncfile instanceof NetcdfDataset)
      ncd = (NetcdfDataset) ncfile;
    else
      ncd = new NetcdfDataset( ncfile);

    // Output the document, use standard formatter
    fmt = new XMLOutputter("  ", true);
    fmt.setLineSeparator("\n");
    fmt.output( makeDocument(location), os);
  }

  Document makeDocument( String location) {
    Element rootElem = new Element("netcdf", ncNS);
    Document doc = new Document(rootElem);

      // namespaces
    rootElem.addNamespaceDeclaration(ncNS);
    /* rootElem.addNamespaceDeclaration(xsiNS);

    if (ncfile instanceof NetcdfDataset)
      rootElem.setAttribute("schemaLocation",
        "http://www.ucar.edu/schemas/netcdf-2.2 http://www.unidata.ucar.edu/schemas/netcdfCS-2.2.xsd", xsiNS);
    else
      rootElem.setAttribute("schemaLocation",
        "http://www.ucar.edu/schemas/netcdf-2.2 http://www.unidata.ucar.edu/schemas/netcdf-2.2.xsd", xsiNS);
    */

    if (null != location)
      rootElem.setAttribute("location", location);
    else
      rootElem.setAttribute("location", ncd.getLocation());

    if (null != ncd.getId())
      rootElem.setAttribute("id", ncd.getId());

    if (null != ncd.getTitle())
      rootElem.setAttribute("title", ncd.getTitle());

    Group rootGroup = ncd.getRootGroup();

    if (ncd.getCoordSysWereAdded()) {
      rootGroup.addAttribute( new Attribute("Conventions", "_Coordinates"));
      String aliases = getCoordinateVariableAliases( ncd);
      if (aliases != null)
        rootGroup.addAttribute( new Attribute("_CoordinateVariableAliases", aliases));
    }

    makeGroup( rootElem, rootGroup);

    ///////////////////////////////////////////////////////////
    // reference systems

      // find common coordinate transforms

      /* ReferenceSystem rs = cs.getReferenceSystem();
      int idx = commonRS.indexOf(rs);
      if (idx >= 0) {
        cs.setReferenceSystem( (ReferenceSystem) commonRS.get(idx)); // point to common one
        continue;
      }
      for (int j=i+1; j<csys.size(); j++) {
        CoordinateSystem cs2 = (CoordinateSystem) csys.get(j);
        ReferenceSystem rs2 = cs2.getReferenceSystem();
        if (rs.equals(rs2)) { // used more than once
          commonRS.add( rs);
          break;
        }
      }
    }

      /* find common reference systems
    ArrayList commonRS = new ArrayList();
    List csys = ncd.getCoordinateSystems();
    for (int i=0; i<csys.size(); i++) {
      CoordinateSystem cs = (CoordinateSystem) csys.get(i);
      ReferenceSystem rs = cs.getReferenceSystem();
      int idx = commonRS.indexOf(rs);
      if (idx >= 0) {
        cs.setReferenceSystem( (ReferenceSystem) commonRS.get(idx)); // point to common one
        continue;
      }
      for (int j=i+1; j<csys.size(); j++) {
        CoordinateSystem cs2 = (CoordinateSystem) csys.get(j);
        ReferenceSystem rs2 = cs2.getReferenceSystem();
        if (rs.equals(rs2)) { // used more than once
          commonRS.add( rs);
          break;
        }
      }
    }

      // find common horiz reference systems
    ArrayList commonHRS = new ArrayList();
    csys = ncd.getCoordinateSystems();
    for (int i=0; i<csys.size(); i++) {
      CoordinateSystem cs = (CoordinateSystem) csys.get(i);
      ReferenceSystem rs = cs.getReferenceSystem();
      ReferenceSystem.Horiz hrs = rs.getHoriz();
      int idx = commonHRS.indexOf(hrs);
      if (idx >= 0) {
        rs.setHoriz( (ReferenceSystem.Horiz) commonHRS.get(idx)); // point to common one
        continue;
      }
      for (int j=i+1; j<csys.size(); j++) {
        CoordinateSystem cs2 = (CoordinateSystem) csys.get(j);
        ReferenceSystem rs2 = cs2.getReferenceSystem();
        ReferenceSystem.Horiz hrs2 = rs2.getHoriz();
        if (hrs.equals(hrs2)) { // used more than once
          commonHRS.add( hrs);
          hrs.setId( hrs.getProjectionName()); // LOOK wrong
          break;
        }
      }
    } */

    /* if (addCoords) {

      // coordinate systems
      List csys2 = ncd.getCoordinateSystems();
      for (int i=0; i<csys2.size(); i++) {
        rootElem.addContent( makeCoordSys( (CoordinateSystem) csys2.get(i)));
      }

        // look for coordinate transforms
      ArrayList coordTrans = new ArrayList();
      ArrayList refSys = new ArrayList();
      List csys = ncd.getCoordinateSystems();
      for (int i=0; i<csys.size(); i++) {
        CoordinateSystem cs = (CoordinateSystem) csys.get(i);
        List ctList = cs.getCoordinateTransforms();
        if (ctList != null) {
          for (int j=0; j<ctList.size(); j++) {
            CoordinateTransform ct = (CoordinateTransform) ctList.get(j);
            if (!coordTrans.contains( ct))
              coordTrans.add(ct);
            /* ReferenceSystem rs = ct.getReferenceSystem();
            if ((rs != null) & !refSys.contains( rs))
              refSys.add(rs);
          }
        }
      }

        // coordinate transforms
      for (int i=0; i<coordTrans.size(); i++)
        rootElem.addContent( makeCoordTransform( (CoordinateTransform) coordTrans.get(i)));


         /* reference systems
      for (int i=0; i<refSys.size(); i++)
        rootElem.addContent( makeReferenceSys( (ReferenceSystem) refSys.get(i))); */

      /* do common reference systems
      for (int i=0; i<commonRS.size(); i++) {
        ReferenceSystem rs = (ReferenceSystem) commonRS.get(i);
        rootElem.addContent( makeReferenceSys( rs, commonHRS));
      }
    } */

    return doc;
  }

  private String getCoordinateVariableAliases(NetcdfDataset ds ) {
    StringBuffer sbuff = new StringBuffer();
    List axes = ds.getCoordinateAxes();
    for (int i = 0; i < axes.size(); i++) {
      CoordinateAxis axis = (CoordinateAxis) axes.get(i);
      Dimension dim;
      if (null != (dim = axis.getCoordinateDimension())) {
        if (!axis.getName().equals(dim.getName()))
          sbuff.append( axis.getName()+" ");
      }
    }

    return (sbuff.length() > 0) ? sbuff.toString() : null;
  }

  private Element makeAttribute( ucar.nc2.Attribute att, String elementName) {
    Element attElem = new Element(elementName, ncNS);
    attElem.setAttribute("name", att.getName());

    DataType dt = att.getDataType();
    if (dt != null)
      attElem.setAttribute("type", dt.toString());

    if (att.isString()) {
      String value = att.getStringValue();
      String err = org.jdom.Verifier.checkCharacterData(value);
      if (err != null) {
        value = "NcMLWriter invalid attribute value, err= "+err;
        System.out.println(value);
      }
      attElem.setAttribute("value", StringUtil.quoteXmlAttribute(value));
    } else {

      StringBuffer buff = new StringBuffer();
      for (int i=0; i<att.getLength(); i++) {
        Number val = att.getNumericValue(i);
        if (i > 0) buff.append( " ");
        buff.append( val.toString());
      }
      attElem.setAttribute("value", buff.toString());
    }
    return attElem;
  }

  private Element makeCoordSys( CoordinateSystem cs) {
    Element csElem = new Element("coordinateSystem", ncNS);
    csElem.setAttribute("name", cs.getName());

    /* ReferenceSystem rs = cs.getReferenceSystem();
    if (rs != null) {
      if (commonRS.contains( rs)) {
        Element rsElem = new Element("referenceSystemRef", ncNS);
        rsElem.setAttribute("ref", rs.getId());
        csElem.addContent( rsElem);
      } else {
        csElem.addContent( makeReferenceSys(rs, commonHRS));
      }
    } */

    ArrayList axes = cs.getCoordinateAxes();
    for (int i=0; i<axes.size(); i++) {
      Element axisElem = new Element("coordinateAxisRef", ncNS);
      axisElem.setAttribute("ref", ((VariableEnhanced) axes.get(i)).getName());
      csElem.addContent( axisElem);
    }

    ArrayList transforms = cs.getCoordinateTransforms();
    if (transforms != null)
    for (int i=0; i<transforms.size(); i++) {
      CoordinateTransform ct = (CoordinateTransform) transforms.get(i);
      if (ct == null) continue;
      Element tElem = new Element("coordinateTransformRef", ncNS);
      tElem.setAttribute("ref", ct.getName());
      csElem.addContent( tElem);
    }
    return csElem;
  }

  private Element makeCoordTransform( CoordinateTransform coordTransform) {
    Element elem = new Element("coordinateTransform", ncNS);
    elem.setAttribute("name", coordTransform.getName());
    elem.setAttribute("authority", coordTransform.getAuthority());
    //if (coordTransform.getReferenceSystem() != null)
    //  elem.setAttribute("referenceCoordinateSystem", coordTransform.getReferenceSystem().getName());
    if (coordTransform.getTransformType() != null)
      elem.setAttribute("transformType", coordTransform.getTransformType().toString());

    ArrayList params = coordTransform.getParameters();
    for (int i=0; i<params.size(); i++) {
      ucar.unidata.util.Parameter p = (ucar.unidata.util.Parameter) params.get(i);
      elem.addContent( makeParameter(p, "parameter"));
    }
    return elem;
  }

  // shared dimensions
  private Element makeDim( Dimension dim) {
    Element dimElem = new Element("dimension", ncNS);
    dimElem.setAttribute("name", dim.getName());
    dimElem.setAttribute("length", Integer.toString(dim.getLength()));
    if (dim.isUnlimited())
      dimElem.setAttribute("isUnlimited", "true");
    if (dim.isUnknown())
      dimElem.setAttribute("isUnknown", "true");
    return dimElem;
  }

  private Element makeGroup( Element elem, Group group) {

      // dimensions
    Iterator dims = group.getDimensions().iterator();
    while ( dims.hasNext()) {
      Dimension dim = (Dimension) dims.next();
      elem.addContent( makeDim( dim));
    }

       // attributes
    Iterator atts = group.getAttributes().iterator();
    while ( atts.hasNext()) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) atts.next();
      elem.addContent( makeAttribute( att, "attribute"));
    }

    /* if (addCoords) {
         // coordinate axes
      Iterator vars = group.getVariables().iterator();
      while ( vars.hasNext()) {
        VariableEnhanced var = (VariableEnhanced) vars.next();
        if (var instanceof CoordinateAxis)
          elem.addContent( makeCoordinateAxis( (CoordinateAxis) var));
      }
    } */

        // regular variables
    Iterator vars = group.getVariables().iterator();
    while ( vars.hasNext()) {
      VariableEnhanced var = (VariableEnhanced) vars.next();
      /* if (addCoords && (var instanceof CoordinateAxis))
        continue; // skip if already added */

      elem.addContent( makeVariable( var));
    }

       // nested groups
    Iterator groups = group.getGroups().iterator();
    while ( groups.hasNext()) {
      Group g = (Group) groups.next();
      Element groupElem = new Element("group", ncNS);
      groupElem.setAttribute("name", g.getShortName());
      elem.addContent( makeGroup( groupElem, g));
    }

    return elem;
  }

  private Element makeParameter( ucar.unidata.util.Parameter p, String elementName) {
    Element attElem = new Element(elementName, ncNS);
    attElem.setAttribute("name", p.getName());
    attElem.setAttribute("type", p.isString() ? "String" : "double");

    if (p.isString()) {
      String value = p.getStringValue();
      String err = org.jdom.Verifier.checkCharacterData(value);
      if (err != null) {
        value = "NcMLWriter invalid attribute value, err= "+err;
        System.out.println(value);
      }
      attElem.setAttribute("value", value);
    } else {

      StringBuffer buff = new StringBuffer();
      for (int i=0; i<p.getLength(); i++) {
        double val = p.getNumericValue(i);
        if (i > 0) buff.append( " ");
        buff.append( val);
      }
      attElem.setAttribute("value", buff.toString());
    }
    return attElem;
  }


  /* private Element makeReferenceSys( ReferenceSystem referenceSystem) {
    Element elem = new Element("referenceCoordinateSystem", ncNS);
    elem.setAttribute("name", referenceSystem.getName());
    elem.setAttribute("authority", referenceSystem.getAuthority());
    if (referenceSystem.getReferenceType() != null)
      elem.setAttribute("type", referenceSystem.getReferenceType().toString());

    ArrayList params = referenceSystem.getParameters();
    for (int i=0; i<params.size(); i++) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) params.get(i);
      elem.addContent( makeAttribute(att, "parameter"));
    }
    return elem;
  } */


  private Element makeVariable( VariableEnhanced var) {
    boolean isStructure = var instanceof Structure;

    Element varElem = new Element(isStructure ? "structure" : "variable", ncNS);
    varElem.setAttribute("name", var.getShortName());

    StringBuffer buff = new StringBuffer();
    List dims = var.getDimensions();
    for (int i=0; i<dims.size(); i++) {
      Dimension dim = (Dimension) dims.get(i);
      if (i > 0) buff.append( " ");
      if (dim.isShared())
        buff.append( dim.getName());
      else
        buff.append( dim.getLength());
    }
    if (buff.length() > 0)
      varElem.setAttribute("shape", buff.toString());

    DataType dt = var.getDataType();
    if (dt != null)
      varElem.setAttribute("type", dt.toString());

        // attributes
    Iterator atts = var.getAttributes().iterator();
    while ( atts.hasNext()) {
      ucar.nc2.Attribute att = (ucar.nc2.Attribute) atts.next();
      varElem.addContent( makeAttribute( att, "attribute"));
    }

    if (var.isMetadata())
      varElem.addContent( makeValues( var));

    if (isStructure) {
      Structure s = (Structure) var;
      Iterator nested = s.getVariables().iterator();
      while ( nested.hasNext()) {
        VariableEnhanced nestedV = (VariableEnhanced) nested.next();
        varElem.addContent( makeVariable( nestedV));
      }
    }

    return varElem;
  }

 /* private Element makeCoordinateAxis( CoordinateAxis var) {
    var.addAttribute( new Attribute("units", var.getUnitsString()));
    if (var.getAxisType() != null)
      var.addAttribute( new Attribute("_CoordinateAxisType", var.getAxisType().toString()));
    if (var.getPositive() != null)
      var.addAttribute( new Attribute("_CoordinateZisPositive", var.getPositive()));

    return makeVariable(var);
  } */

  private Element makeValues( VariableEnhanced v) {
    Element elem = new Element("values", ncNS);

    StringBuffer buff = new StringBuffer();
    Array a = null;
    try {
      a = v.read();
    } catch (IOException ioe) {
      return elem;
    }

    if (v.getDataType() == DataType.CHAR) {
      char[] data = (char []) a.getStorage();
      elem.setText(new String(data));

    } else if (v.getDataType() == DataType.STRING) { // use seperate elements??
      IndexIterator iter = a.getIndexIterator();
      int count = 0;
      while (iter.hasNext()) {
        String s =  (String) iter.getObjectNext();
        if (count++ > 0) buff.append(" ");
        buff.append( "\""+s+"\"");
      }
      elem.setText(buff.toString());

    } else {
       //check to see if regular
      if ((a.getRank() == 1) && (a.getSize() > 2)) {
        Index ima = a.getIndex();
        double start = a.getDouble( ima.set(0));
        double incr = a.getDouble( ima.set(1)) - start;
        boolean isRegular = true;
        for (int i=2; i < a.getSize(); i++) {
          double v1 = a.getDouble( ima.set(i));
          double v0 = a.getDouble( ima.set(i-1));
          if (!closeEnough(v1-v0, incr))
            isRegular = false;
        }

        if (isRegular) {
          elem.setAttribute("start", Double.toString( start));
          elem.setAttribute("increment", Double.toString( incr));
          elem.setAttribute("npts", Long.toString( v.getSize()));
          return elem;
        }
      }

      // not regular
      boolean isRealType = (v.getDataType() == DataType.DOUBLE) || (v.getDataType() == DataType.FLOAT);
      IndexIterator iter = a.getIndexIterator();
      buff.append(isRealType ? iter.getDoubleNext() : iter.getIntNext());
      while (iter.hasNext()) {
        buff.append(" ");
        buff.append(isRealType ? iter.getDoubleNext() : iter.getIntNext());
      }
      elem.setText(buff.toString());

    } // not string

    return elem;
  }

  private boolean closeEnough( double d1, double d2) {
    return Math.abs(d2-d1) < 1.0e-7;
  }

  public static void main( String arg[]){
    String urls = "C:/data/conventions/coards/cldc.mean.nc";
    try {
      NetcdfDataset df = NetcdfDataset.openDataset(urls);
      NcMLWriter writer = new NcMLWriter();
      System.out.println("NetcdfDataset = "+urls+"\n"+df);
      System.out.println("-----------");
      writer.writeXML( df, System.out, null);
    } catch (Exception ioe) {
      System.out.println("error = "+urls);
      ioe.printStackTrace();
    }
  }

}

/* Change History:
   $Log: NcMLWriter.java,v $
   Revision 1.5  2004/12/08 18:08:32  caron
   implement _CoordinateAliasForDimension

   Revision 1.4  2004/12/07 01:29:32  caron
   redo convention parsing, use _Coordinate encoding.

   Revision 1.3  2004/12/03 04:46:27  caron
   no message

   Revision 1.2  2004/12/01 05:53:43  caron
   ncml pass 2, new convention parsing

   Revision 1.1  2004/11/21 01:16:47  caron
   ncml pass 1

   Revision 1.4  2004/10/29 00:14:10  caron
   no message

   Revision 1.3  2004/10/06 19:03:41  caron
   clean up javadoc
   change useV3 -> useRecordsAsStructure
   remove id, title, from NetcdfFile constructors
   add "in memory" NetcdfFile

   Revision 1.2  2004/09/28 21:28:07  caron
   use Parameter

   Revision 1.1  2004/08/16 20:53:48  caron
   2.2 alpha (2)

   Revision 1.4  2003/09/02 22:27:59  caron
   NcML dataset version 2

   Revision 1.3  2003/06/26 22:27:14  caron
   check invalid attribute characters

   Revision 1.2  2003/05/29 23:46:03  caron
   showCoords only if 1D

   Revision 1.1  2003/04/08 15:06:24  caron
   nc2 version 2.1

 */



