// $Id: Giniheader.java,v 1.8 2004/12/15 22:35:25 caron Exp $
/*
 * Copyright 1997-2004 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, strlenwrite to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package ucar.nc2.iosp.gini;


import ucar.nc2.*;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.util.Parameter;
import ucar.ma2.Array;


import java.io.*;

import java.util.*;

import java.text.*;
import java.nio.*;


/**
 * Netcdf header reading and writing for version 3 file format.
 * This is used by Giniiosp.
 */

class Giniheader {
  static final byte[] MAGIC = new byte[] {0x43, 0x44, 0x46, 0x01 };
  static final int MAGIC_DIM = 10;
  static final int MAGIC_VAR = 11;
  static final int MAGIC_ATT = 12;




  private boolean debug = false, debugPos = false, debugString = false, debugHeaderSize = false;

  private ucar.unidata.io.RandomAccessFile raf;
  private ucar.nc2.NetcdfFile ncfile;
  private PrintStream out = System.out;

  int numrecs = 0; // number of records written
  int recsize = 0; // size of each record (padded)
  int dataStart = 0; // where the data starts
  int recStart = 0; // where the record data starts
  int GINI_PIB_LEN = 21;   // gini product indentification block
  int GINI_PDB_LEN = 512;  // gini product description block
  int GINI_HED_LEN = 533;  // gini product header
  double DEG_TO_RAD = 0.017453292;
  byte Z_DEFLATED = 8;
  byte DEF_WBITS = 15;
  private long actualSize, calcSize;
  protected int Z_type = 0;



   public boolean isValidFile( ucar.unidata.io.RandomAccessFile raf)
   {
    try
    {
        this.actualSize = raf.length();
    }
    catch ( IOException e )
    {
        return( false );
    }

    try{
        int pib = this.readPIB( raf );
        if( pib != 21 ) return false; // not unidata radar mosiac gini file
    }
    catch ( IOException e )
    {
        return( false );
    }

    return true;
   }

    /**
   * Read the header and populate the ncfile
   * @param raf

   * @throws IOException
   */
   int readPIB(ucar.unidata.io.RandomAccessFile raf ) throws IOException {
    this.raf = raf;

    long pos = 0;
    raf.seek(pos);

    // gini header process
    byte[] b = new byte[GINI_PIB_LEN];
    raf.read(b);
    String pib = new String(b);

    if( !pib.startsWith("TICZ")) return (int)pos; // gini header start with TICZ 99....

    pos = pib.indexOf( "KNES" );
    if ( pos == -1 ) pos = pib.indexOf ( "CHIZ" );

    if ( pos != -1 ) {                    /* 'KNES' or 'CHIZ' found         */
       pos = pib.indexOf ( "\r\r\n" );    /* <<<<< UPC mod 20030710 >>>>>   */
       if ( pos != -1 ) {                 /* CR CR NL found             */
          pos  = pos + 3;
       }
    } else {
      pos = 0;
    }

     return (int)pos;
  }

  void read(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile, PrintStream out) throws IOException {
    this.raf = raf;
    this.ncfile = ncfile;
    int        proj;                        /* projection type indicator     */
                                            /* 1 - Mercator                  */
                                            /* 3 - Lambert Conf./Tangent Cone*/
                                            /* 5 - Polar Stereographic       */

    int        ent_id;                      /* GINI creation entity          */
    int        sec_id;                      /* GINI sector ID                */
    int        sss_id;                      /* GINI channel ID               */
    int        phys_elem;                   /* 1 - Visible, 2- 3.9IR, 3 - 6.7IR ..*/
    int        nx;
    int        ny;
    int        pole;
    int        gyear;
    int        gmonth;
    int        gday;
    int        ghour;
    int        gminute;
    int        gsecond;
    double     lonv;                        /* meridian parallel to y-axis */
    double     diff_lon;
    double     lon1 = 0.0, lon2;
    long       hoff = 0;


    hoff = readPIB(raf );
    if (out != null) this.out = out;
    actualSize = raf.length();

    Attribute att = new Attribute( "Conventions", "GRIB");
    this.ncfile.addAttribute(null, att);

    long pos = GINI_PIB_LEN;
    raf.seek(pos);

    byte[] b2 = new byte[2];

    //sat_id = (int )( raf.readByte());
    Byte nv = new Byte(raf.readByte());
    att = new Attribute( "source_id", nv);
    this.ncfile.addAttribute(null, att);

    nv = new Byte( raf.readByte());
    ent_id = nv.intValue();
    att = new Attribute( "entity_id", nv);
    this.ncfile.addAttribute(null, att);

    nv = new Byte( raf.readByte());
    sss_id = nv.intValue();
    att = new Attribute( "sector_id", nv);
    this.ncfile.addAttribute(null, att);

    nv = new Byte ( raf.readByte());
    phys_elem = nv.intValue();
    att = new Attribute( "phys_elem", nv);
    this.ncfile.addAttribute(null, att);

    raf.skipBytes(4);

    gyear = (int) ( raf.readByte());
      gyear += ( gyear < 50 ) ? 2000 : 1900;
    gmonth = (int) ( raf.readByte());
    gday = (int) ( raf.readByte());
    ghour = (int) ( raf.readByte());
    gminute = (int) ( raf.readByte());
    gsecond = (int) ( raf.readByte());

    DateFormat dformat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    Calendar cal = Calendar.getInstance();
    cal.set(gyear, gmonth-1, gday, ghour, gminute, gsecond);
    String dstring = dformat.format(cal.getTime());


    att = new Attribute( "Time", dstring);
    this.ncfile.addAttribute(null, att);

    raf.readByte();   /* skip a byte for hundreds of seconds */

    nv   = new Byte ( raf.readByte());
    att = new Attribute( "ProjIndex", nv);
    this.ncfile.addAttribute(null, att);
    proj = nv.intValue();
    if( proj == 1) {
     att = new Attribute( "ProjName", "MERCATOR");
    } else if (proj == 3) {
     att = new Attribute( "ProjName", "LAMBERT_CONFORNAL");
    } else if (proj == 5) {
     att = new Attribute( "ProjName", "POLARSTEREOGRAPGIC");
    }

    this.ncfile.addAttribute(null, att);

    /*
    ** Get grid dimensions
    */

    byte[] b3 = new byte[3];

    raf.read(b2, 0, 2);
    nx = getInt( b2, 2);
    Integer ni = new Integer(nx);
    att = new Attribute( "NX", ni);
    this.ncfile.addAttribute(null, att);


    raf.read(b2, 0, 2);
    ny = getInt( b2, 2);
    ni = new Integer(ny);
    att = new Attribute( "NY", ni);
    this.ncfile.addAttribute(null, att);

    ProjectionImpl projection = null;
    double dxKm = 0.0, dyKm = 0.0, latin = 0.0, latFirstPoint = 0.0, lonProjectionOrigin = 0.0;

    switch( proj ) {

      case 1:                                                    /* Mercator */
        /*
        ** Get the latitude and longitude of first and last "grid" points
        */

        /* Latitude of first grid point */
        raf.read(b3, 0, 3);
        int nn = getInt( b3, 3);
        Double nd = new Double(((double) nn) / 10000.0);
        att = new Attribute( "Latitude0", nd);
        this.ncfile.addAttribute(null, att);

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        nd = new Double(((double) nn) / 10000.0 * (-1));
        lon1 = nd.doubleValue();
        att = new Attribute( "Longitude0", nd);
        this.ncfile.addAttribute(null, att);

        /* Longitude of last grid point */
        raf.readByte(); /* skip one byte */

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        nd = new Double( ((double) nn) / 10000.0);
        att = new Attribute( "LatitudeN", nd);
        this.ncfile.addAttribute(null, att);

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        nd = new Double(((double) nn) / 10000.0 * (-1));
        lon2 = nd.doubleValue();
        att = new Attribute( "LongitudeN", nd);
        this.ncfile.addAttribute(null, att);

        /*
        ** Hack to catch incorrect sign of lon2 in header.
        */

        if ( lon1 > 0.0 && lon2 < 0.0 ) lon2 *= -1;

        /*
        ** Get the "Latin" parameter.  The ICD describes this value as:
        ** "Latin - The latitude(s) at which the Mercator projection cylinder
        ** intersects the earth."  It should read that this is the latitude
        ** at which the image resolution is that defined by octet 41.
        */
        raf.readInt(); /* skip 4 bytes */
        raf.readByte();

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        nd = new Double(((double) nn) / 10000.0);

        /* Latitude of proj cylinder intersects */
        att = new Attribute( "LatitudeX", nd);
        this.ncfile.addAttribute(null, att);


        break;

      case 3:                               /* Lambert Conformal             */
      case 5:                               /* Polar Stereographic           */


        /*
        ** Get lat/lon of first grid point
        */
        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        latFirstPoint = (((double) nn) / 10000.0);

        nd = new Double(((double) nn) / 10000.0);
        att = new Attribute( "Lat1", nd);
        this.ncfile.addAttribute(null, att);

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        lon1 = (((double) nn) / 10000.0);

        nd = new Double(((double) nn) / 10000.0);
        lon1 = nd.doubleValue();
        att = new Attribute( "Lon1", nd);
        this.ncfile.addAttribute(null, att);

        /*
        ** Get Lov - the orientation of the grid; i.e. the east longitude of
        ** the meridian which is parallel to the y-aixs
        */
        raf.readByte(); /* skip one byte */

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        lonv = (((double) nn) / 10000.0);
        lonProjectionOrigin = lonv;

        nd = new Double(((double) nn) / 10000.0);
        lonv = nd.doubleValue();
        att = new Attribute( "Lov", nd);
        this.ncfile.addAttribute(null, att);

        /*
        ** Get distance increment of grid
        */
        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        dxKm = ((double) nn) / 10000.0;

        nd = new Double(((double) nn) / 10000.);
        att = new Attribute( "DxKm", nd);
        this.ncfile.addAttribute(null, att);

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        dyKm = ((double) nn) / 10000.0;

        nd = new Double( ((double) nn) / 10000.);
        att = new Attribute( "DyKm", nd);
        this.ncfile.addAttribute(null, att);

        /* Convert to east longitude */
        if ( lonv < 0. ) lonv += 360.;
        if ( lon1 < 0. ) lon1 += 360.;

        diff_lon = lonv - lon1;

        if ( diff_lon >  180. )  diff_lon -= 360.;
        if ( diff_lon < -180. )  diff_lon += 360.;


        /*
        ** Convert to normal longitude to McIDAS convention
        */

        lonv = (lonv > 180.) ? (360.-lonv) : -lonv;

        /*
        ** Check high bit of octet for North or South projection center
        */

        nv= new Byte(raf.readByte());
        pole = nv.intValue();
        pole = ( pole > 127 ) ? -1 : 1;
        ni = new Integer(pole);
        att = new Attribute( "ProjCenter", ni);
        this.ncfile.addAttribute(null, att);

        raf.readByte(); /* skip one byte for Scanning mode */

        raf.read(b3, 0, 3);
        nn = getInt( b3, 3);
        latin = (((double) nn) / 10000.);

        nd = new Double(((double) nn) / 10000.);
        att = new Attribute( "Latin", nd);
        this.ncfile.addAttribute(null, att);

        if (proj == 3)
          projection = new LambertConformal(latin, lonProjectionOrigin, latin, latin);

        break;

      default:
        System.out.println("unimplemented projection");


    }

    /** Get the image resolution.
    */
    raf.seek(pos + 41);  /* jump to 42 bytes of PDB */
    nv = new Byte( ( raf.readByte()) );      /* Res [km] */
    att = new Attribute( "imageResolution", nv);
    this.ncfile.addAttribute(null, att);

    /* compression flag */
    int tempp = (int)raf.getFilePointer();
    nv = new Byte( ( raf.readByte()));      /* Res [km] */
    att = new Attribute( "compressionFlag", nv );
    this.ncfile.addAttribute(null, att);

    if ( convertunsignedByte2Short( nv.byteValue() ) == 128 )
    {
       Z_type = 1;
       //out.println( "ReadNexrInfo:: This is a Z file ");
    }

    // only one data variable per gini file

    String vname= gini_GetPhysElemID(phys_elem);
    Variable var = new Variable( ncfile, ncfile.getRootGroup(), null, vname);
    var.addAttribute( new Attribute("long_name", getPhysElemLongName(phys_elem)));
    var.addAttribute( new Attribute("units", getPhysElemUnits(phys_elem)));
    // var.addAttribute( new Attribute("missing_value", new Byte((byte) 0))); // ??

      // get dimensions
    int velems = 1;
    boolean isRecord = false;
    int rank = 2;
    ArrayList dims = new ArrayList();

    Dimension dimX  = new Dimension( "x", nx, true, false, false);
    Dimension dimY  = new Dimension( "y", ny, true, false, false);

    ncfile.addDimension( null, dimY);
    ncfile.addDimension( null, dimX);

    velems = dimX.getLength() * dimY.getLength();

    dims.add( dimY);
    dims.add( dimX);

    var.setDimensions( dims);

    // data type
    var.setDataType( DataType.BYTE);
    var.addAttribute(new Attribute("_unsigned", "true"));


      // size and beginning data position in file
    int vsize = velems;
    long begin = hoff + GINI_PDB_LEN ;
    if (debug) out.println(" name= "+vname+" vsize="+vsize+" velems="+velems+" begin= "+begin+" isRecord="+isRecord+"\n");
    var.setSPobject( new Vinfo (vsize, begin, isRecord, nx, ny));

    ncfile.addVariable(null, var);

    // add coordinate information. we need:
    // nx, ny, dx, dy,
    // latin, lov, la1, lo1

    // we have to project in order to find the origin
    ProjectionPointImpl start = (ProjectionPointImpl) projection.latLonToProj( new LatLonPointImpl( latFirstPoint, lon1));
    if (debug) System.out.println("start at proj coord "+start);

    double startx = start.getX();
    double starty = start.getY();

    // create coordinate variables
    Variable xaxis = new Variable( ncfile, null, null, "x");
    xaxis.setDataType( DataType.DOUBLE);
    xaxis.setDimensions( "x");
    xaxis.addAttribute( new Attribute("long_name", "projection x coordinate"));
    xaxis.addAttribute( new Attribute("units", "km"));
    xaxis.addAttribute( new Attribute("_CoordinateAxisType", "GeoX"));
    double[] data = new double[nx];
    for (int i = 0; i < data.length; i++)
      data[i] = startx + i*dxKm;
    Array dataA = Array.factory(DataType.DOUBLE.getPrimitiveClassType(), new int[] {nx}, data);
    xaxis.setCachedData( dataA, false);
    ncfile.addVariable(null, xaxis);

    Variable yaxis = new Variable( ncfile, null, null, "y");
    yaxis.setDataType( DataType.DOUBLE);
    yaxis.setDimensions( "y");
    yaxis.addAttribute( new Attribute("long_name", "projection y coordinate"));
    yaxis.addAttribute( new Attribute("units", "km"));
    yaxis.addAttribute( new Attribute("_CoordinateAxisType", "GeoY"));
    data = new double[ny];
    double endy = starty + dyKm * data.length; // apparently lat1,lon1 is always the lower ledt, but data is upper left
    for (int i = 0; i < data.length; i++)
      data[i] = endy - i*dyKm;
    dataA = Array.factory(DataType.DOUBLE.getPrimitiveClassType(), new int[] {ny}, data);
    yaxis.setCachedData( dataA, false);
    ncfile.addVariable(null, yaxis);

    // coordinate transform variable
    Variable ct = new Variable( ncfile, null, null, projection.getClassName());
    ct.setDataType( DataType.CHAR);
    ct.setDimensions( "");
    List params = projection.getProjectionParameters();
    for (int i = 0; i < params.size(); i++) {
      Parameter p = (Parameter) params.get(i);
      ct.addAttribute( new Attribute(p));
    }
    ct.addAttribute( new Attribute("_CoordinateTransformType", "Projection"));
    ct.addAttribute( new Attribute("_CoordinateAxes", "x y"));
    // fake data
    dataA = Array.factory(DataType.CHAR.getPrimitiveClassType(), new int[] {});
    dataA.setChar(dataA.getIndex(), ' ');
    ct.setCachedData(dataA, false);

    ncfile.addVariable(null, ct);
    ncfile.addAttribute( null, new Attribute("Conventions", "_Coordinates"));

    // finish
    ncfile.finish();
  }




  int gini_GetCompressType( )
  {
       return Z_type;
  }

  // Return the string of entity ID for the GINI image file

  String gini_GetEntityID(int ent_id )
  {
    String name;
     /* GINI channel ID          */

    switch( ent_id ) {
      case 1:
        name = "RADAR-MOSIAC";
        break;
      case 9:
        name = "METEOSAT";
        break;
      case 10:
        name = "GOES-7";
        break;
      case 11:
        name = "GOES-8" ;
        break;
      case 12:
        name = "GOES-9" ;
        break;
      case 13:
        name = "GOES-10";
        break;
      case 14:
        name = "GOES-11";
        break;
      case 15:
        name = "GOES-12";
        break;
      default:
        name = "Unknown-ID";
	}

   return name;


  }


  // Return the channel ID for the GINI image file

  String gini_GetPhysElemID(int phys_elem )
  {
    String name;
     /* GINI channel ID          */

    switch( phys_elem ) {
      case 1:
        name = "Imager_Visible";
        break;
      case 2:
        name = "Imager_3dot9_micron_IR";
        break;
      case 3:
        name = "Imager_6dot7/6dot5_micron_IR";
        break;
      case 4:
        name = "Imager_11_micron_IR" ;
        break;
      case 5:
        name = "Imager_12_micron_IR" ;
        break;
      case 6:
        name = "Imager_13_micron_IR";
        break;
      case 7:
        name = "Imager_1dot3_micron_IR";
        break;
      case 27:
      case 28:
        name = "BaseReflectivity";
        break;
      case 30:
        name = "1hourPrecipitation";
        break;
      case 31:
        name = "totalPrecipitation";
        break;
      default:
        name = "Unknown";
	}

   return name;


  }

  // ??
  String getPhysElemUnits(int phys_elem ) {
    switch( phys_elem ) {
      case 27: return "dbZ";
      case 28: return "dbZ";
      case 30: return "IN";
      case 31: return "IN";
      default: return "";
    }
  }

  // Return the channel ID for the GINI image file

  String getPhysElemLongName(int phys_elem ) {
     switch( phys_elem ) {
      case 28:
      case 27: return "Base Reflectivity Composition (Unidata)";
      case 30: return "1 hour precipitation Composition (Unidata)";
      case 31: return "total precipitation Composition (Unidata)";
      default: return "unknown";
    }
  }


/*
** Name:       GetInt
**
** Purpose:    Convert GINI 2 or 3-byte quantities to int
**
*/
  int getInt( byte[] b, int num )
  {
      int            base=1;
      int            i;
      int            word=0;

      int bv[] = new int[num];

      for (i = 0; i<num; i++ )
      {
        bv[i] = convertunsignedByte2Short(b[i]);
      }

      if( bv[0] > 127 )
      {
         bv[0] -= 128;
         base = -1;
      }
      /*
      ** Calculate the integer value of the byte sequence
      */

      for ( i = num-1; i >= 0; i-- ) {
        word += base * bv[i];
        base *= 256;
      }

      return word;

  }

  public short convertunsignedByte2Short(byte b)
  {
     return (short)((b<0)? (short)b + 256 : (short)b);
  }


// this converts a byte array to a wrapped primitive (Byte, Short, Integer, Double, Float, Long)
  protected Object convert( byte[] barray, DataType dataType, int byteOrder) {

    if (dataType == DataType.BYTE) {
      return new Byte( barray[0]);
    }

    if (dataType == DataType.CHAR) {
      return new Character((char) barray[0]);
    }

    ByteBuffer bbuff = ByteBuffer.wrap( barray);
    if (byteOrder >= 0)
      bbuff.order( byteOrder == ucar.unidata.io.RandomAccessFile.LITTLE_ENDIAN? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);

    if (dataType == DataType.SHORT) {
      ShortBuffer tbuff = bbuff.asShortBuffer();
      return new Short(tbuff.get());

    } else if (dataType == DataType.INT) {
      IntBuffer tbuff = bbuff.asIntBuffer();
      return new Integer(tbuff.get());

    } else if (dataType == DataType.LONG) {
      LongBuffer tbuff = bbuff.asLongBuffer();
      return new Long(tbuff.get());

    } else if (dataType == DataType.FLOAT) {
      FloatBuffer tbuff = bbuff.asFloatBuffer();
      return new Float(tbuff.get());

    } else if (dataType == DataType.DOUBLE) {
      DoubleBuffer tbuff = bbuff.asDoubleBuffer();
      return new Double(tbuff.get());
    }

    throw new IllegalStateException();
  }

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

  // variable info for reading/writing
  static class Vinfo {
    int vsize; // size of array in bytes. if isRecord, size per record.
    long begin; // offset of start of data from start of file
    boolean isRecord; // is it a record variable?
    int nx;
    int ny;
    Vinfo( int vsize, long begin, boolean isRecord, int x, int y) {
      this.vsize = vsize;
      this.begin = begin;
      this.isRecord = isRecord;
      this.nx = x;
      this.ny = y;
    }
  }


  int isZlibHed( byte[] buf ){
    short b0 = convertunsignedByte2Short(buf[0]);
    short b1 = convertunsignedByte2Short(buf[1]);

    if ( (b0 & 0xf) == Z_DEFLATED ) {
      if ( (b0 >> 4) + 8 <= DEF_WBITS ) {
        if ( (((b0 << 8) + b1) % 31)==0 ) {
          return 1;
        }
      }
    }

    return 0;

  }

}
/* Change History:
   $Log: Giniheader.java,v $
   Revision 1.8  2004/12/15 22:35:25  caron
   add _unsigned

   Revision 1.7  2004/12/07 22:13:28  yuanho
   add phyElem for 1hour and total precipitation

   Revision 1.6  2004/12/07 22:13:15  yuanho
   add phyElem for 1hour and total precipitation

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

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

   Revision 1.3  2004/10/19 15:17:22  yuanho
   gini header DxKm update

   Revision 1.2  2004/10/15 23:18:34  yuanho
   gini projection update

   Revision 1.1  2004/10/13 22:57:57  yuanho
   no message

   Revision 1.4  2004/08/16 20:53:45  caron
   2.2 alpha (2)

   Revision 1.3  2004/07/12 23:40:17  caron
   2.2 alpha 1.0 checkin

   Revision 1.2  2004/07/06 19:28:10  caron
   pre-alpha checkin

   Revision 1.1.1.1  2003/12/04 21:05:27  caron
   checkin 2.2

 */