/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.iosp.sigmet;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import ucar.ma2.Range;
import ucar.ma2.Array;
import ucar.ma2.ArrayFloat;
import ucar.ma2.ArrayInt;
import ucar.ma2.DataType;
import ucar.ma2.Index;
import ucar.ma2.IndexIterator;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Section;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.iosp.AbstractIOServiceProvider;
import ucar.nc2.iosp.Layout;
import ucar.nc2.iosp.LayoutRegular;
import ucar.unidata.io.RandomAccessFile;

/**
 * Implementation of the ServerProvider pattern provides input/output
 * of the SIGMET-IRIS dataset. IOSP are managed by the NetcdfFile class.
 * When the SigmetDataset is requested by calling NetcdfFile.open(), the file
 * is opened as a ucar.unidata.io.RandomAccessFile.
 * The SIGMET-IRIS data format are described in "IRIS Programmer's Manual" ch.3
 * The SIGMET-IRIS file consists of records with fixed length=6144 bytes. Data is written in
 * little-endian format. The organization of raw product SIGMET-IRIS file is:
 * Record #1   { <product_hdr> 0,0,0,...}
 * Record #2   { <ingest_header> 0,0,0,... }
 * ---if there are several sweeps (usually 24) and one type of data:
 * Record #3   { <raw_prod_bhdr><ingest_data_header>...Data for sweep 1.. }
 * Record #4   { <raw_prod_bhdr>...Data for sweep 1...  }
 * .............................................
 * Record #n   { <raw_prod_bhdr>...Data for sweep 1... 0.... }
 * Record #n+1 { <raw_prod_bhdr><ingest_data_header>...Data for sweep 2.. }
 * Record #n+2 { <raw_prod_bhdr>...Data for sweep 2...  }
 * .............................................
 * Record #m   { <raw_prod_bhdr>...Data for sweep 2... 0...  }
 * Record #m+1 { <raw_prod_bhdr><ingest_data_header>...Data for sweep 3.. }
 * .............................................
 * Structure of "Data for sweep" is:  <ray_header><ray_data>...<ray_header><ray_data>...
 * <ray_header> and <ray_data> are encoded with the compression algorithm
 * ("IRIS Programmer's Manual" 3.5.4.1)
 * ---if there are "n" types of data (usually 4 or 5) and one sweep:
 * Record #3   { <raw_prod_bhdr><ingest_data_header(data_type_1)><ingest_data_header(data_type_2)>...
 * <ingest_data_header(data_type_n)>...Data...}
 * Record #4   { <raw_prod_bhdr>...Data...  }
 * .............................................
 * Record #n   { <raw_prod_bhdr>...Data...  }
 * Structure of "Data" is:  <ray_header(data_type_1)><ray_data(data_type_1)><ray_header(data_type_2)><ray_data(data_type_2)>...
 * <ray_header(data_type_n)><ray_data(data_type_n)><ray_header(data_type_1)><ray_data(data_type_1)>
 * <ray_header(data_type_2)><ray_data(data_type_2)>... <ray_header(data_type_n)><ray_data(data_type_n)>...
 * <ray_header> and <ray_data> are encoded with the compression algorithm
 * ("IRIS Programmer's Manual" 3.5.4.1)
 *
 * @author yuanho
 * @see "ftp://ftp.sigmet.com/outgoing/manuals/program/3data.pdf"
 *   esp section 3.5
 */

public class SigmetIOServiceProvider extends AbstractIOServiceProvider {
  private ArrayList<Variable> varList = null;
  private int[] tsu_sec = null;
  private int[] sweep_bins = null;
  private String date0;

  public static java.util.Map<String, Number> recHdr = new java.util.HashMap<String, Number>();
  private SigmetVolumeScan volScan;

  public static void main(String[] args) {
    String infile = " ";
    if (args.length == 1) {
      infile = args[0];
    } else {
      System.out.println("Usage: java SigmetIOServiceProvider inputFile");
      System.exit(0);
    }
    try {
      NetcdfFile.registerIOProvider(SigmetIOServiceProvider.class);
      NetcdfFile ncfile = NetcdfFile.open(infile);
      System.out.println("ncfile = \n" + ncfile);
    } catch (Exception e) {
      System.out.println("MAIN!!!   " + e.toString());
      e.printStackTrace();
    }
  }

  public String getFileTypeDescription() {
    return "SIGMET-IRIS";
  }

  public String getFileTypeVersion() {
    return "SIGMET-IRIS";
  }

  public String getFileTypeId() {
    return "SIGMET";
  }

  /**
   * Check if this is a valid SIGMET-IRIS file for this IOServiceProvider.
   */
  public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) {
    try {
      raf.order(RandomAccessFile.LITTLE_ENDIAN);
      // The first struct in the file is the product_hdr, which will have the
      // standard structure_header, followed by other embedded structures.
      // Each of these structures also have a structure header. To validate
      // the file we check for a product_hdr (by looking for type 27 in the
      // structure_header), then a product_configuration structure (by looking
      // for type 26 in its structure_header), then checking that that
      // the product_configuration does indicate a type of RAW data (type 15)
      raf.seek(0);
      short[] data = new short[13];
      raf.readShort(data, 0, 13);
      return (data[0] == (short) 27 &&
              data[6] == (short) 26 &&
              data[12] ==(short) 15);
    } catch (IOException ioe) {
      System.out.println("In isValidFile(): " + ioe.toString());
      return false;
    }
  }

  /**
   * Open existing file, and populate ncfile with it.
   */
  public void open(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile,
                   ucar.nc2.util.CancelTask cancelTask) throws java.io.IOException {
    super.open(raf, ncfile, cancelTask);
    //java.util.Map<String, Number> recHdr=new java.util.HashMap<String, Number>();
    java.util.Map<String, String> hdrNames = new java.util.HashMap<String, String>();
    volScan = new SigmetVolumeScan(raf, ncfile, varList);
    this.varList = init(raf, ncfile, hdrNames);

    // doData(raf, ncfile, varList);
    // raf.close();
    // this.ncfile.close();
  }

  /**
   * Read some global data from SIGMET file. The SIGMET file consists of records with
   * fixed length=6144 bytes.
   */
  static public java.util.Map<String, Number> readRecordsHdr(ucar.unidata.io.RandomAccessFile raf) {
    java.util.Map<String, Number> recHdr1 = new java.util.HashMap<String, Number>();
    try {
      int nparams = 0;
      //      -- Read from <product_end> of the 1st record -- 12+320+120
      //      -- Calculate Nyquist velocity --------------------
      raf.seek(452);
      int prf = raf.readInt();
      raf.seek(480);
      int wave = raf.readInt();
      float vNyq = calcNyquist(prf, wave);
      recHdr1.put("vNyq", vNyq);

      //      -- Read from the 2nd record----------- 6144+12(strucr_hdr)+168(from ingest_config)
      raf.seek(6324);
      int radar_lat = raf.readInt();
      int radar_lon = raf.readInt();           //6328
      short ground_height = raf.readShort();  //6332
      short radar_height = raf.readShort();   //6334
      raf.skipBytes(4);
      short num_rays = raf.readShort();      // 6340
      raf.skipBytes(2);
      int radar_alt = raf.readInt();          //6344
      raf.seek(6648);
      int time_beg = raf.readInt();
      raf.seek(6652);
      int time_end = raf.readInt();
      raf.seek(6772);
      int data_mask = raf.readInt();
      for (int j = 0; j < 32; j++) {
        nparams += ((data_mask >> j) & (0x1));
      }
      raf.seek(6912);
      short multiprf = raf.readShort();
      raf.seek(7408);
      int range_first = raf.readInt();   //  cm    7408
      int range_last = raf.readInt();    //  cm  7412
      raf.skipBytes(2);
      short bins = raf.readShort();             //7418
      if (bins % 2 != 0) bins = (short) (bins + 1);
      raf.skipBytes(4);
      int step = raf.readInt();          //  cm    7424
      raf.seek(7574);
      short number_sweeps = raf.readShort();    // 7574
      raf.seek(12312);
      int base_time = raf.readInt();        //<ingest_data_header> 3d rec
      raf.skipBytes(2);
      short year = raf.readShort();
      short month = raf.readShort();
      short day = raf.readShort();
      recHdr1.put("radar_lat", calcAngle(radar_lat));
      recHdr1.put("radar_lon", calcAngle(radar_lon));
      recHdr1.put("range_first", range_first);
      recHdr1.put("range_last", range_last);
      recHdr1.put("ground_height", ground_height);
      recHdr1.put("radar_height", radar_height);
      recHdr1.put("radar_alt", radar_alt);
      recHdr1.put("step", step);
      recHdr1.put("bins", bins);    //System.out.println("  bins="+bins);
      recHdr1.put("num_rays", num_rays); //System.out.println("  rays="+num_rays);
      recHdr1.put("nparams", nparams);//System.out.println("  nparams="+nparams);
      recHdr1.put("multiprf", multiprf);
      recHdr1.put("number_sweeps", number_sweeps);   //System.out.println("IN HDR:  number_sweeps="+number_sweeps);
      recHdr1.put("year", year);
      recHdr1.put("month", month);
      recHdr1.put("day", day);
      recHdr1.put("base_time", base_time);
    } catch (Exception e) {
      System.out.println(e.toString());
      e.printStackTrace();
    }
    return recHdr1;
  }

  /**
   * Read StationName strings
   */
  public java.util.Map<String, String> readStnNames(ucar.unidata.io.RandomAccessFile raf) {
    java.util.Map<String, String> hdrNames = new java.util.HashMap<String, String>();
    try {
      raf.seek(6288);
      String stnName = raf.readString(16);  //System.out.println(" stnName="+stnName.trim());
      raf.seek(6306);
      String stnName_util = raf.readString(16);
      hdrNames.put("StationName", stnName.trim());
      hdrNames.put("StationName_SetupUtility", stnName_util.trim());
    } catch (Exception e) {
      System.out.println(e.toString());
      e.printStackTrace();
    }
    return hdrNames;
  }

  /**
   * Define Dimensions, Variables, Attributes in ncfile
   *
   * @param raf      ucar.unidata.io.RandomAccessFile corresponds of SIGMET datafile.
   * @param ncfile   an empty NetcdfFile object which will be filled.
   * @param hdrNames java.util.Map with values for "StationName.." Attributes
   * @return ArrayList of Variables of ncfile
   */
  public ArrayList<Variable> init(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile,
                                  java.util.Map<String, String> hdrNames) throws java.io.IOException {
    // prepare attribute values
    String[] data_name = {" ", "TotalPower", "Reflectivity", "Velocity",
            "Width", "Differential_Reflectivity"};
    String[] unit = {" ", "dbZ", "dbZ", "m/sec", "m/sec", "dB"};
    int[] type = {1, 2, 3, 4, 5};
    String def_datafile = "SIGMET-IRIS";
    String tim = "";
    int ngates = 0;

    recHdr = readRecordsHdr(raf);
    hdrNames = readStnNames(raf);

    String stnName = hdrNames.get("StationName");
    String stnName_util = hdrNames.get("StationName_SetupUtility");
    float radar_lat = recHdr.get("radar_lat").floatValue(); //System.out.println("rad_lat="+radar_lat);
    float radar_lon = recHdr.get("radar_lon").floatValue(); //System.out.println("rad_lon="+radar_lon);
    short ground_height = recHdr.get("ground_height").shortValue(); //System.out.println("ground_H="+ground_height);
    short radar_height = recHdr.get("radar_height").shortValue(); //System.out.println("radar_H="+radar_height);
    int radar_alt = (recHdr.get("radar_alt").intValue()) / 100; //System.out.println("rad_alt="+radar_alt);
    short num_rays = recHdr.get("num_rays").shortValue(); //System.out.println("num_rays="+num_rays);
    short bins = recHdr.get("bins").shortValue(); //System.out.println("bins="+bins);
    float range_first = (recHdr.get("range_first").intValue()) * 0.01f; //System.out.println("range_1st="+range_first);
    float range_last = (recHdr.get("range_last").intValue()) * 0.01f; //System.out.println("step="+step);
    short number_sweeps = recHdr.get("number_sweeps").shortValue();
    //System.out.println("number_sweeps="+number_sweeps);
    int nparams = (recHdr.get("nparams").intValue()); //System.out.println("nparams="+nparams);
    short year = recHdr.get("year").shortValue(); //System.out.println("year="+year);
    short month = recHdr.get("month").shortValue();
    short day = recHdr.get("day").shortValue();
    int base_time = (recHdr.get("base_time").intValue());

    // define number of gates for every sweep
    sweep_bins = new int[nparams * number_sweeps];
    if (number_sweeps > 1) {
      sweep_bins = volScan.getNumberGates();
    } else {
      for (int kk = 0; kk < nparams; kk++) {
        sweep_bins[kk] = bins;
      }
    }

    // add Dimensions
    Dimension scanR = new Dimension("scanR", number_sweeps, true);
    Dimension radial = new Dimension("radial", num_rays, true);
    Dimension[] gateR = new Dimension[number_sweeps];
    String dim_name = "gateR";
    for (int j = 0; j < number_sweeps; j++) {
      if (number_sweeps > 1) {
        dim_name = "gateR_sweep_" + (j + 1);
      }
      gateR[j] = new Dimension(dim_name, sweep_bins[j], true);
    }
    ncfile.addDimension(null, scanR);
    ncfile.addDimension(null, radial);
    for (int j = 0; j < number_sweeps; j++) {
      ncfile.addDimension(null, gateR[j]);
    }
    ArrayList<Dimension> dims0 = new ArrayList<Dimension>();
    ArrayList<Dimension> dims1 = new ArrayList<Dimension>();
    ArrayList<Dimension> dims2 = new ArrayList<Dimension>();
    ArrayList<Dimension> dims3 = new ArrayList<Dimension>();

    ArrayList<Variable> varList = new ArrayList<Variable>();

    Variable[][] v = new Variable[nparams][number_sweeps];
    String var_name = "";
    for (int j = 0; j < nparams; j++) {
      int tp = type[j];
      var_name = data_name[tp];
      for (int jj = 0; jj < number_sweeps; jj++) {
        if (number_sweeps > 1) {
          var_name = data_name[tp] + "_sweep_" + (jj + 1);
        }
        v[j][jj] = new Variable(ncfile, null, null, var_name);
        v[j][jj].setDataType(DataType.FLOAT);
        dims2.add(radial);
        dims2.add(gateR[jj]);
        v[j][jj].setDimensions(dims2);
        v[j][jj].addAttribute(new Attribute(CDM.LONG_NAME, var_name));
        v[j][jj].addAttribute(new Attribute(CDM.UNITS, unit[tp]));
        String coordinates = "time elevationR azimuthR distanceR";
        v[j][jj].addAttribute(new Attribute(_Coordinate.Axes, coordinates));
        v[j][jj].addAttribute(new Attribute(CDM.MISSING_VALUE, -999.99f));
        ncfile.addVariable(null, v[j][jj]);
        varList.add(v[j][jj]);
        dims2.clear();
      }
    }
    tsu_sec = new int[number_sweeps];
    String[] tsu = new String[number_sweeps];
    String[] time_units = new String[number_sweeps];
    tsu_sec = volScan.getStartSweep();
    for (int i = 0; i < number_sweeps; i++) {
      String st1 = Short.toString(month);
      if (st1.length() < 2) st1 = "0" + st1;
      String st2 = Short.toString(day);
      if (st2.length() < 2) st2 = "0" + st2;
      date0 = String.valueOf(year) + "-" + st1 + "-" + st2;
      tsu[i] = date0 + "T" + calcTime(tsu_sec[i], 0) + "Z";
    }
    for (int j = 0; j < number_sweeps; j++) {
      time_units[j] = "secs since " + tsu[j];
    }

    dims0.add(radial);
    // add "time" variable
    Variable[] time = new Variable[number_sweeps];
    String tm = "time";
    String tm_name = "";
    for (int j = 0; j < number_sweeps; j++) {
      tm_name = tm;
      if (number_sweeps > 1) {
        tm_name = tm + "_sweep_" + (j + 1);
      }
      time[j] = new Variable(ncfile, null, null, tm_name);
      time[j].setDataType(DataType.INT);
      time[j].setDimensions(dims0);
      time[j].addAttribute(new Attribute(CDM.LONG_NAME, "time from start of sweep"));
      time[j].addAttribute(new Attribute(CDM.UNITS, time_units[j]));
      time[j].addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));
      time[j].addAttribute(new Attribute(CDM.MISSING_VALUE, -99));
      ncfile.addVariable(null, time[j]);
      varList.add(time[j]);
    }

    // add "elevationR" variable
    Variable[] elevationR = new Variable[number_sweeps];
    String ele = "elevationR";
    String ele_name = "";
    for (int j = 0; j < number_sweeps; j++) {
      ele_name = ele;
      if (number_sweeps > 1) {
        ele_name = ele + "_sweep_" + (j + 1);
      }
      elevationR[j] = new Variable(ncfile, null, null, ele_name);
      elevationR[j].setDataType(DataType.FLOAT);
      elevationR[j].setDimensions(dims0);
      elevationR[j].addAttribute(new Attribute(CDM.LONG_NAME, "elevation angle"));
      elevationR[j].addAttribute(new Attribute(CDM.UNITS, "degrees"));
      elevationR[j].addAttribute(new Attribute(_Coordinate.AxisType, AxisType.RadialElevation.toString()));
      elevationR[j].addAttribute(new Attribute(CDM.MISSING_VALUE, -999.99f));
      ncfile.addVariable(null, elevationR[j]);
      varList.add(elevationR[j]);
    }

    // add "azimuthR" variable
    Variable[] azimuthR = new Variable[number_sweeps];
    String azim = "azimuthR";
    String azim_name = "";
    for (int j = 0; j < number_sweeps; j++) {
      azim_name = azim;
      if (number_sweeps > 1) {
        azim_name = azim + "_sweep_" + (j + 1);
      }
      azimuthR[j] = new Variable(ncfile, null, null, azim_name);
      azimuthR[j].setDataType(DataType.FLOAT);
      azimuthR[j].setDimensions(dims0);
      azimuthR[j].addAttribute(new Attribute(CDM.LONG_NAME, "azimuth angle"));
      azimuthR[j].addAttribute(new Attribute(CDM.UNITS, "degrees"));
      azimuthR[j].addAttribute(new Attribute(_Coordinate.AxisType, AxisType.RadialAzimuth.toString()));
      azimuthR[j].addAttribute(new Attribute(CDM.MISSING_VALUE, -999.99f));
      ncfile.addVariable(null, azimuthR[j]);
      varList.add(azimuthR[j]);
    }

    // add "distanceR" variable
    Variable[] distanceR = new Variable[number_sweeps];
    String dName = "distanceR";
    String dist_name = "";
    for (int j = 0; j < number_sweeps; j++) {
      dist_name = dName;
      if (number_sweeps > 1) {
        dist_name = dName + "_sweep_" + (j + 1);
      }
      distanceR[j] = new Variable(ncfile, null, null, dist_name);
      distanceR[j].setDataType(DataType.FLOAT);
      dims1.add(gateR[j]);
      distanceR[j].setDimensions(dims1);
      distanceR[j].addAttribute(new Attribute(CDM.LONG_NAME, "radial distance"));
      distanceR[j].addAttribute(new Attribute(CDM.UNITS, "m"));
      distanceR[j].addAttribute(new Attribute(_Coordinate.AxisType, AxisType.RadialDistance.toString()));
      ncfile.addVariable(null, distanceR[j]);
      varList.add(distanceR[j]);
      dims1.clear();
    }
    // add "numGates" variable
    dims3.add(scanR);
    Variable numGates = new Variable(ncfile, null, null, "numGates");
    numGates.setDataType(DataType.INT);
    numGates.setDimensions(dims3);
    numGates.addAttribute(new Attribute(CDM.LONG_NAME, "number of gates in the sweep"));
    ncfile.addVariable(null, numGates);
    varList.add(numGates);

    // add global attributes
    ncfile.addAttribute(null, new Attribute("definition", "SIGMET-IRIS RAW"));
    ncfile.addAttribute(null, new Attribute("description", "SIGMET-IRIS data are reading by Netcdf IOSP"));
    ncfile.addAttribute(null, new Attribute("StationName", stnName));
    ncfile.addAttribute(null, new Attribute("StationName_SetupUtility", stnName_util));
    ncfile.addAttribute(null, new Attribute("radar_lat", radar_lat));
    ncfile.addAttribute(null, new Attribute("radar_lon", radar_lon));
    ncfile.addAttribute(null, new Attribute("ground_height", ground_height));
    ncfile.addAttribute(null, new Attribute("radar_height", radar_height));
    ncfile.addAttribute(null, new Attribute("radar_alt", radar_alt));
    ncfile.addAttribute(null, new Attribute("num_data_types", nparams));
    ncfile.addAttribute(null, new Attribute("number_sweeps", number_sweeps));
    String sn = "start_sweep";
    String snn = "";
    for (int j = 0; j < number_sweeps; j++) {
      snn = sn;
      if (number_sweeps > 1) {
        snn = sn + "_" + (j + 1);
      }
      ncfile.addAttribute(null, new Attribute(snn, tsu[j]));
    }
    ncfile.addAttribute(null, new Attribute("num_rays", num_rays));
    ncfile.addAttribute(null, new Attribute("max_number_gates", bins));
    ncfile.addAttribute(null, new Attribute("range_first", range_first));
    ncfile.addAttribute(null, new Attribute("range_last", range_last));
    ncfile.addAttribute(null, new Attribute("DataType", "Radial"));
    ncfile.addAttribute(null, new Attribute(CDM.CONVENTIONS, _Coordinate.Convention));

    // --------- fill all of values in the ncfile ------
    doNetcdfFileCoordinate(ncfile, volScan.base_time, volScan.year, volScan.month, volScan.day, varList, recHdr);

    ncfile.finish();

    return varList;
  }

  /**
   * Fill all of the variables/attributes in the ncfile
   *
   * @param ncfile  NetcdfFile object which will be filled.
   * @param bst     number of seconds since midnight for start of sweep
   * @param yr      year of start of each sweep
   * @param m       month of start of each sweep
   * @param dda     day of start of each sweep
   * @param varList ArrayList of Variables of ncfile
   * @param recHdr  java.util.Map with values for Attributes
   */
  public void doNetcdfFileCoordinate(ucar.nc2.NetcdfFile ncfile, int[] bst,
                                     short[] yr, short[] m, short[] dda,
                                     ArrayList<Variable> varList, java.util.Map<String, Number> recHdr) {
    // prepare attribute values

    String[] unit = {" ", "dbZ", "dbZ", "m/sec", "m/sec", "dB"};
    String def_datafile = "SIGMET-IRIS";
    Short header_length = 80;
    Short ray_header_length = 6;
    int ngates = 0;

    float radar_lat = recHdr.get("radar_lat").floatValue(); //System.out.println("rad_lat="+radar_lat);
    float radar_lon = recHdr.get("radar_lon").floatValue(); //System.out.println("rad_lon="+radar_lon);
    short ground_height = recHdr.get("ground_height").shortValue(); //System.out.println("ground_H="+ground_height);
    short radar_height = recHdr.get("radar_height").shortValue(); //System.out.println("radar_H="+radar_height);
    int radar_alt = (recHdr.get("radar_alt").intValue()) / 100; //System.out.println("rad_alt="+radar_alt);
    short num_rays = recHdr.get("num_rays").shortValue(); //System.out.println("HERE!! num_rays="+num_rays);
    float range_first = (recHdr.get("range_first").intValue()) * 0.01f; //System.out.println("range_1st="+range_first);
    float range_last = (recHdr.get("range_last").intValue()) * 0.01f; //System.out.println("step="+step);
    short number_sweeps = recHdr.get("number_sweeps").shortValue();
    int nparams = (recHdr.get("nparams").intValue()); //System.out.println("nparams="+nparams);
    // define date/time
    //int last_t=(int)(ray[nparams*number_sweeps-1][num_rays-1].getTime());
    int last_t = volScan.lastRay.getTime();
    String sss1 = Short.toString(m[0]);
    if (sss1.length() < 2) sss1 = "0" + sss1;
    String sss2 = Short.toString(dda[0]);
    if (sss2.length() < 2) sss2 = "0" + sss2;
    String base_date0 = String.valueOf(yr[0]) + "-" + sss1 + "-" + sss2;
    String sss11 = Short.toString(m[number_sweeps - 1]);
    if (sss11.length() < 2) sss11 = "0" + sss11;
    String sss22 = Short.toString(dda[number_sweeps - 1]);
    if (sss22.length() < 2) sss22 = "0" + sss22;
    String base_date1 = String.valueOf(yr[number_sweeps - 1]) + "-" + sss11 + "-" + sss22;
    String start_time = base_date0 + "T" + calcTime(bst[0], 0) + "Z";
    String end_time = base_date1 + "T" + calcTime(bst[number_sweeps - 1], last_t) + "Z";
    ncfile.addAttribute(null, new Attribute("time_coverage_start", start_time));
    ncfile.addAttribute(null, new Attribute("time_coverage_end", end_time));

    // set all of Variables
    try {
      int sz = varList.size();

      ArrayFloat.D2[] dataArr = new ArrayFloat.D2[nparams * number_sweeps];
      Index[] dataIndex = new Index[nparams * number_sweeps];

      Ray[] rtemp = new Ray[(int) num_rays];

      // NCdump.printArray(dataArr[0], "Total_Power", System.out, null);

      Variable[] distanceR = new Variable[number_sweeps];
      ArrayFloat.D1[] distArr = new ArrayFloat.D1[number_sweeps];
      Index[] distIndex = new Index[number_sweeps];
      String distName = "distanceR";
      for (int i = 0; i < number_sweeps; i++) {
        if (number_sweeps > 1) {
          distName = "distanceR_sweep_" + (i + 1);
        }
        for (Variable aVarList : varList) {
          if ((aVarList.getShortName()).equals(distName.trim())) {
            distanceR[i] = aVarList;
            break;
          }
        }
        distArr[i] = (ArrayFloat.D1) Array.factory(DataType.FLOAT, distanceR[i].getShape());
        distIndex[i] = distArr[i].getIndex();

        // for (int jj=0; jj<num_rays; jj++) { rtemp[jj]=ray[i][jj]; }
        ngates = sweep_bins[i];
        float stp = calcStep(range_first, range_last, (short) ngates);
        for (int ii = 0; ii < ngates; ii++) {
          distArr[i].setFloat(distIndex[i].set(ii), (range_first + ii * stp));
        }
      }
      // NCdump.printArray(distArr[0], "distanceR", System.out, null);
      List rgp = volScan.getTotalPowerGroups();
      if (rgp.size() == 0) rgp = volScan.getReflectivityGroups();
      List[] sgp = new ArrayList[number_sweeps];
      for (int i = 0; i < number_sweeps; i++) {
        sgp[i] = (List) rgp.get((short) i);
      }

      Variable[] time = new Variable[number_sweeps];
      ArrayInt.D1[] timeArr = new ArrayInt.D1[number_sweeps];
      Index[] timeIndex = new Index[number_sweeps];
      String t_n = "time";
      for (int i = 0; i < number_sweeps; i++) {
        if (number_sweeps > 1) {
          t_n = "time_sweep_" + (i + 1);
        }
        for (Variable aVarList : varList) {
          if ((aVarList.getShortName()).equals(t_n.trim())) {
            time[i] = aVarList;
            break;
          }
        }

        //                if (time[i].getShape().length == 0) {
        //                    continue;
        //                }
        timeArr[i] = (ArrayInt.D1) Array.factory(DataType.INT, time[i].getShape());
        timeIndex[i] = timeArr[i].getIndex();
        List rlist = sgp[i];

        for (int jj = 0; jj < num_rays; jj++) {
          rtemp[jj] = (Ray) rlist.get(jj);
        }    //ray[i][jj]; }
        for (int jj = 0; jj < num_rays; jj++) {
          timeArr[i].setInt(timeIndex[i].set(jj), rtemp[jj].getTime());
        }
      }

      // NCdump.printArray(timeArr[0], "time", System.out, null);

      Variable[] azimuthR = new Variable[number_sweeps];
      ArrayFloat.D1[] azimArr = new ArrayFloat.D1[number_sweeps];
      Index[] azimIndex = new Index[number_sweeps];
      String azimName = "azimuthR";
      for (int i = 0; i < number_sweeps; i++) {
        if (number_sweeps > 1) {
          azimName = "azimuthR_sweep_" + (i + 1);
        }
        for (Variable aVarList : varList) {
          if ((aVarList.getShortName()).equals(azimName.trim())) {
            azimuthR[i] = aVarList;
            break;
          }
        }
        azimArr[i] = (ArrayFloat.D1) Array.factory(DataType.FLOAT, azimuthR[i].getShape());
        azimIndex[i] = azimArr[i].getIndex();
        List rlist = sgp[i];

        for (int jj = 0; jj < num_rays; jj++) {
          rtemp[jj] = (Ray) rlist.get(jj);
        } //ray[i][jj]; }
        for (int jj = 0; jj < num_rays; jj++) {
          azimArr[i].setFloat(azimIndex[i].set(jj), rtemp[jj].getAz());
        }
      }
      //NCdump.printArray(azimArr[0], "azimuthR", System.out, null);

      Variable[] elevationR = new Variable[number_sweeps];
      ArrayFloat.D1[] elevArr = new ArrayFloat.D1[number_sweeps];
      Index[] elevIndex = new Index[number_sweeps];
      String elevName = "elevationR";
      for (int i = 0; i < number_sweeps; i++) {
        if (number_sweeps > 1) {
          elevName = "elevationR_sweep_" + (i + 1);
        }
        for (Variable aVarList : varList) {
          if ((aVarList.getShortName()).equals(elevName.trim())) {
            elevationR[i] = aVarList;
            break;
          }
        }
        elevArr[i] = (ArrayFloat.D1) Array.factory(DataType.FLOAT, elevationR[i].getShape());
        elevIndex[i] = elevArr[i].getIndex();
        List rlist = sgp[i];

        for (int jj = 0; jj < num_rays; jj++) {
          rtemp[jj] = (Ray) rlist.get(jj);
        } //ray[i][jj]; }
        for (int jj = 0; jj < num_rays; jj++) {
          elevArr[i].setFloat(elevIndex[i].set(jj), rtemp[jj].getElev());
        }
      }
      // NCdump.printArray(elevArr[0], "elevationR", System.out, null);

      Variable numGates = null;
      for (int i = 0; i < number_sweeps; i++) {
        for (Variable aVarList : varList) {
          if ((aVarList.getShortName()).equals("numGates")) {
            numGates = aVarList;
            break;
          }
        }
      }
      ArrayInt.D1 gatesArr = (ArrayInt.D1) Array.factory(DataType.INT, numGates.getShape());
      Index gatesIndex = gatesArr.getIndex();

      for (int i = 0; i < number_sweeps; i++) {
        List rlist = sgp[i];
        for (int jj = 0; jj < num_rays; jj++) {
          rtemp[jj] = (Ray) rlist.get(jj);
        } //ray[i][jj]; }
        ngates = rtemp[0].getBins();
        gatesArr.setInt(gatesIndex.set(i), ngates);
      }

      for (int i = 0; i < number_sweeps; i++) {
        distanceR[i].setCachedData(distArr[i], false);
        time[i].setCachedData(timeArr[i], false);
        azimuthR[i].setCachedData(azimArr[i], false);
        elevationR[i].setCachedData(elevArr[i], false);
      }
      numGates.setCachedData(gatesArr, false);
      // startSweep.setCachedData(sweepArr, false);

      //          -------------------------------------------------
      // int b=(int)ray[0][0].getBins();

      // -- Test of readData() and readToByteChannel() -----------------
      /*
Range r1=new Range(356, 359);
Range r2=new Range(0, 15);
java.util.List arlist=new ArrayList();
arlist.add(r1);
arlist.add(r2);
Array testArr=readData(v[0], new Section(arlist));
NCdump.printArray(testArr, "Total_Power_sweep_1", System.out, null);
WritableByteChannel channel=new FileOutputStream(new File("C:\\netcdf\\tt.dat")).getChannel();
long ikk=readToByteChannel(v[0], new Section(arlist), channel);
System.out.println("IKK="+ikk);
channel.close();
      */
      //---------------------------------------------------

    } catch (Exception e) {
      System.out.println(e.toString());
      e.printStackTrace();
    }
  }   //----------- end of doNetcdf ----------------------------------

  /**
   * Read data from a top level Variable and return a memory resident Array.
   *
   * @param v2      Variable. It may have FLOAT/INTEGER data type.
   * @param section wanted section of data of Variable. The section list is a list
   *                of ucar.ma2.Range which define the requested data subset.
   * @return Array of data which will be read from Variable through this call.
   */
  public Array readData1(ucar.nc2.Variable v2, Section section)
          throws IOException, InvalidRangeException {
    //doData(raf, ncfile, varList);
    int[] sh = section.getShape();
    Array temp = Array.factory(v2.getDataType(), sh);
    long pos0 = 0;
    // Suppose that the data has LayoutRegular
    LayoutRegular index = new LayoutRegular(pos0, v2.getElementSize(), v2.getShape(), section);
    if (v2.getShortName().startsWith("time") | v2.getShortName().startsWith("numGates")) {
      temp = readIntData(index, v2);
    } else {
      temp = readFloatData(index, v2);
    }
    return temp;
  }

  public Array readData(Variable v2, Section section) throws IOException, InvalidRangeException {
    // Vgroup vgroup = (Vgroup) v2.getSPobject();
    // Range scanRange = section.getRange(0);
    // Range radialRange = section.getRange(1);
    // Range gateRange = section.getRange(2);

    Array data = Array.factory(v2.getDataType(), section.getShape());
    IndexIterator ii = data.getIndexIterator();

    List<List<Ray>> groups;
    String shortName = v2.getShortName();
    if (shortName.startsWith("Reflectivity"))
      groups = volScan.getReflectivityGroups();
    else if (shortName.startsWith("Velocity"))
      groups = volScan.getVelocityGroups();
    else if (shortName.startsWith("TotalPower"))
      groups = volScan.getTotalPowerGroups();
    else if (shortName.startsWith("Width"))
      groups = volScan.getWidthGroups();
    else if (shortName.startsWith("DiffReflectivity"))
      groups = volScan.getDifferentialReflectivityGroups();
    else
      throw new IllegalStateException("Illegal variable name = "+shortName);

    if (section.getRank() == 2) {
      Range radialRange = section.getRange(0);
      Range gateRange = section.getRange(1);
      List<Ray> lli = groups.get(0);
      readOneScan(lli, radialRange, gateRange, ii);
    } else {
      Range scanRange = section.getRange(0);
      Range radialRange = section.getRange(1);
      Range gateRange = section.getRange(2);

      for (int scanIdx : scanRange) {
        readOneScan( groups.get(scanIdx), radialRange, gateRange, ii);
      }
    }
    return data;
  }

  private void readOneScan(List<Ray> mapScan, Range radialRange, Range gateRange, IndexIterator ii) throws IOException {
    int siz = mapScan.size();
    for (int radialIdx : radialRange) {
      if (radialIdx >= siz)
        readOneRadial(null, gateRange, ii);
      else {
        Ray r = mapScan.get(radialIdx);
        readOneRadial(r, gateRange, ii);
      }
    }
  }

  private void readOneRadial(Ray r, Range gateRange, IndexIterator ii) throws IOException {
    if (r == null) {
      for (int i = 0; i < gateRange.length(); i++)
        ii.setFloatNext(Float.NaN);
      return;
    }
    r.readData(volScan.raf, gateRange, ii);
  }

  /**
   * Read data from a top level Variable of INTEGER data type and return a memory resident Array.
   *
   * @param index LayoutRegular object
   * @param v2    Variable has INTEGER data type.
   * @return Array of data which will be read from Variable through this call.
   */
  public Array readIntData(LayoutRegular index, Variable v2) throws IOException {
    int[] var = (int[]) (v2.read().get1DJavaArray(v2.getDataType()));
    int[] data = new int[(int) index.getTotalNelems()];
    while (index.hasNext()) {
      Layout.Chunk chunk = index.next();
      System.arraycopy(var, (int) chunk.getSrcPos() / 4, data, (int) chunk.getDestElem(), chunk.getNelems());
    }
    return Array.factory(v2.getDataType(), new int[] {(int) index.getTotalNelems()}, data);
  }

  /**
   * Read data from a top level Variable of FLOAT data type and return a memory resident Array.
   *
   * @param index LayoutRegular object
   * @param v2    Variable has FLOAT data type.
   * @return Array of data which will be read from Variable through this call.
   */
  public Array readFloatData(LayoutRegular index, Variable v2)
          throws IOException {
    float[] var = (float[]) (v2.read().get1DJavaArray(v2.getDataType().getPrimitiveClassType()));
    float[] data = new float[(int) index.getTotalNelems()];
    while (index.hasNext()) {
      Layout.Chunk chunk = index.next();
      System.arraycopy(var, (int) chunk.getSrcPos() / 4, data,
              (int) chunk.getDestElem(), chunk.getNelems());
    }
    return Array.factory(v2.getDataType(), new int[]{(int) index.getTotalNelems()}, data);
  }
  //----------------------------------------------------------------------------------

  /**
   * Read data from a top level Variable and send data to a WritableByteChannel.
   *
   * @param v2      Variable
   * @param section wanted section of data of Variable. The section list is a list
   *                of ucar.ma2.Range which define the requested data subset.
   * @param channel WritableByteChannel object - channel that can write bytes.
   * @return the number of bytes written, possibly zero.
   */
  public long readToByteChannel11(ucar.nc2.Variable v2, Section section, WritableByteChannel channel)
          throws java.io.IOException, ucar.ma2.InvalidRangeException {
    Array data = readData(v2, section);
    float[] ftdata = new float[(int) data.getSize()];
    byte[] bytedata = new byte[(int) data.getSize() * 4];
    IndexIterator iter = data.getIndexIterator();
    int i = 0;
    ByteBuffer buffer = ByteBuffer.allocateDirect(bytedata.length);
    while (iter.hasNext()) {
      ftdata[i] = iter.getFloatNext();
      bytedata[i] = new Float(ftdata[i]).byteValue();
      buffer.put(bytedata[i]);
      i++;
    }
    buffer = ByteBuffer.wrap(bytedata);
    // write the bytes to the channel
    int count = channel.write(buffer);
    System.out.println("COUNT=" + count);
    // check if all bytes where written
    if (buffer.hasRemaining()) {
      // if not all bytes were written, move the unwritten bytes to the beginning and
      // set position just after the last unwritten byte
      buffer.compact();
    } else {
      buffer.clear();
    }
    return (long) count;
  }

  //  -----------------------------------------------------------------------

  /**
   * Convert 2 bytes binary angle to float
   *
   * @param angle two bytes binary angle
   * @return float value of angle in degrees with precision of two decimal
   */
  static float calcAngle(short angle) {
    final double maxval = 65536.0;
    double ang = (double) angle;
    if (ang < 0.0) {
      ang = maxval + ang;
    }
    double temp = (ang / maxval) * 360.0;
    BigDecimal bd = new BigDecimal(temp);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Convert 4 bytes binary angle to float
   *
   * @param ang four bytes binary angle
   * @return float value of angle with precision of two decimal in degrees
   */
  static float calcAngle(int ang) {
    final double maxval = 4294967296.0;
    double temp = (ang / maxval) * 360.0;
    BigDecimal bd = new BigDecimal(temp);
    BigDecimal result = bd.setScale(3, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Calculate radial elevation of each ray
   *
   * @param angle two bytes binary angle
   * @return float value of elevation in degrees with precision of two decimal
   */
  static float calcElev(short angle) {
    final double maxval = 65536.0;
    double ang = (double) angle;
    if (angle < 0) ang = (~angle) + 1;
    double temp = (ang / maxval) * 360.0;
    BigDecimal bd = new BigDecimal(temp);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Calculate distance between sequential bins in a ray
   *
   * @param range_first range of first bin in centimeters
   * @param range_last  range of last bin in centimeters
   * @param num_bins    number of bins
   * @return float distance in centimeters with precision of two decimal
   */
  static float calcStep(float range_first, float range_last, short num_bins) {
    float step = (range_last - range_first) / (num_bins - 1);
    BigDecimal bd = new BigDecimal(step);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Calculate azimuth of a ray
   *
   * @param az0 azimuth at beginning of ray (binary angle)
   * @param az1 azimuth at end of ray (binary angle)
   * @return float azimuth in degrees with precision of two decimal
   */
  static float calcAz(short az0, short az1) {
    // output in deg
    float azim0 = calcAngle(az0);
    float azim1 = calcAngle(az1);
    float d = 0.0f;
    d = Math.abs(azim0 - azim1);
    if ((az0 < 0) & (az1 > 0)) {
      d = Math.abs(360.0f - azim0) + Math.abs(azim1);
    }
    double temp = azim0 + d * 0.5;
    if (temp > 360.0) {
      temp -= 360.0;
    }
    BigDecimal bd = new BigDecimal(temp);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Calculate data values from raw ingest data
   *
   * @param recHdr java.util.Map object with values for calculation
   * @param dty    type of data ( "Total_Power", "Reflectivity", "Velocity",
   *               "Width", "Differential_Reflectivity")
   * @param data   1-byte input value
   * @return float value with precision of two decimal
   */
  static float calcData(Map<String, Number> recHdr, short dty, byte data) {
    short[] coef = {1, 2, 3, 4}; // MultiPRF modes
    short multiprf = recHdr.get("multiprf").shortValue();
    float vNyq = recHdr.get("vNyq").floatValue();
    double temp = -999.99;
    switch (dty) {
      default:        // dty=1,2 -total_power, reflectivity (dBZ)
        if (data != 0) {
          temp = (((int) data & 0xFF) - 64) * 0.5;
        }
        break;
      case 3:        // dty=3 - mean velocity (m/sec)
        if (data != 0) {
          temp = ((((int) data & 0xFF) - 128) / 127.0) * vNyq * coef[multiprf];
        }
        break;
      case 4:        // dty=4 - spectrum width (m/sec)
        if (data != 0) {
          double v = ((((int) data & 0xFF) - 128) / 127.0) * vNyq * coef[multiprf];
          temp = (((int) data & 0xFF) / 256.0) * v;
        }
        break;
      case 5:        // dty=5 - differential reflectivity (dB)
        if (data != 0) {
          temp = ((((int) data & 0xFF) - 128) / 16.0);
        }
        break;
    }
    BigDecimal bd = new BigDecimal(temp);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

  /**
   * Calculate time as hh:mm:ss
   *
   * @param t  number of seconds since midnight for start of sweep
   * @param t0 time in seconds from start of sweep
   * @return time as string "hh:mm:ss"
   */
  static String calcTime(int t, int t0) {
    StringBuilder tim = new StringBuilder();
    int[] tt = new int[3];
    int mmh = (t + t0) / 60;
    tt[2] = (t + t0) % 60;                  // Define SEC
    tt[0] = mmh / 60;                     // Define HOUR
    tt[1] = mmh % 60;                     // Define MIN
    for (int i = 0; i < 3; i++) {
      String s = Integer.toString(tt[i]);
      int len = s.length();
      if (len < 2) {
        s = "0" + tt[i];
      }
      if (i != 2) s += ":";
      tim.append(s);
    }
    return tim.toString();
  }

  /**
   * Calculate of Nyquist velocity
   *
   * @param prf  PRF in Hertz
   * @param wave wavelength in 1/100 of centimeters
   * @return float value of Nyquist velocity in m/sec with precision of two decimal
   */
  static float calcNyquist(int prf, int wave) {
    double tmp = (prf * wave * 0.01) * 0.25;
    tmp = tmp * 0.01;                    //Make it m/sec
    BigDecimal bd = new BigDecimal(tmp);
    BigDecimal result = bd.setScale(2, RoundingMode.HALF_DOWN);
    return result.floatValue();
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.dataset;

import ucar.nc2.constants.CF;
import ucar.nc2.dataset.transform.VertTransformBuilderIF;
import ucar.unidata.geoloc.vertical.*;
import ucar.nc2.Dimension;
import javax.annotation.concurrent.Immutable;

/**
 * A VerticalCT is a CoordinateTransform function CT: (GeoZ) -> Height or Pressure.
 * Typically it may be dependent also on X,Y and/or Time. CT: (X,Y,GeoZ,Time) -> Height or Pressure.
 * This class just records the transformation parameters. The mathematical transformation itself is
 * delegated to a class implementing ucar.unidata.geoloc.vertical.VerticalTransform.
 *
 * @author caron
 */
@Immutable
public class VerticalCT extends CoordinateTransform {
  private final VerticalCT.Type type;
  private final VertTransformBuilderIF builder;

  /**
   * Create a Vertical Coordinate Transform.
   *
   * @param name      name of transform, must be unique within the dataset.
   * @param authority naming authority.
   * @param type      type of vertical transform
   * @param builder   creates the VerticalTransform
   */
  public VerticalCT(String name, String authority, VerticalCT.Type type, VertTransformBuilderIF builder) {
    super(name, authority, TransformType.Vertical);
    this.type = type;
    this.builder = builder;
  }

  /**
   * Copy Constructor
   *
   * @param from copy from this one
   */
  public VerticalCT(VerticalCT from) {
    super(from.getName(), from.getAuthority(), from.getTransformType());
    this.type = from.getVerticalTransformType();
    this.builder = from.getBuilder();
  }

  /**
   * get the Vertical Transform type
   *
   * @return the Vertical Transform Type
   */
  public VerticalCT.Type getVerticalTransformType() {
    return type;
  }

  /**
   * Use the builder to make the Vertical Transform function
   *
   * @param ds      containing dataset
   * @param timeDim time Dimension
   * @return VerticalTransform
   * @see VertTransformBuilderIF#makeMathTransform
   */
  public VerticalTransform makeVerticalTransform(NetcdfDataset ds, Dimension timeDim) {
    return builder.makeMathTransform(ds, timeDim, this);
  }

  /**
   * get the CoordTransBuilderIF
   *
   * @return builder
   */
  public VertTransformBuilderIF getBuilder() {
    return builder;
  }

  @Override
  public String toString() {
    return "VerticalCT {" +
            "type=" + type +
            ", builder=" + builder.getTransformName() +
            '}';
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // inner class VerticalCT.Type
  // private static final java.util.Map<String,Type> hash = new java.util.HashMap<>(10);

  /**
   * Enumeration of known Vertical transformations.
   */
  public enum Type {
    // These are from CF-1.0: not all are implemented because we dont have an example to test
    HybridSigmaPressure(CF.atmosphere_hybrid_sigma_pressure_coordinate),
    HybridHeight(CF.atmosphere_hybrid_height_coordinate),
    LnPressure(CF.atmosphere_ln_pressure_coordinate),
    OceanSigma(CF.ocean_sigma_coordinate),
    OceanS(CF.ocean_s_coordinate),
    Sleve(CF.atmosphere_sleve_coordinate),
    Sigma(CF.atmosphere_sigma_coordinate),

    //-Sachin 03/25/09
    OceanSG1("ocean_s_g1"),
    OceanSG2("ocean_s_g2"),

    // others
    Existing3DField("atmosphere_sigma"),
    WRFEta("WRFEta");

    private final String name;

    /**
     * Constructor
     *
     * @param s name of Type
     */
    Type(String s) {
      this.name = s;
    }

    /**
     * Find the VerticalCT.Type that matches this name.
     *
     * @param name find this name
     * @return VerticalCT.Type or null if no match.
     */
    public static Type getType(String name) {
      for (Type t : Type.values()) {
        if (t.name.equalsIgnoreCase(name)) return t;
      }
      return null;
    }

    public String toString() {
      return name;
    }
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.inventory.partition;

import thredds.featurecollection.FeatureCollectionConfig;
import thredds.filesystem.MFileOS;
import thredds.filesystem.MFileOS7;
import thredds.inventory.*;
import ucar.nc2.util.CloseableIterator;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A PartitionManager from a list of index files.
 * Used in GribCollectionBuilder with multiple runtimes.
 * Index Files are already in the cache.
 *
 * @author John
 * @since 2/5/14                   `
 */
public class PartitionManagerFromIndexList extends CollectionAbstract implements PartitionManager {
  private List<MFile> partIndexFiles;
  private final FeatureCollectionConfig config;

  public PartitionManagerFromIndexList(MCollection dcm, List<MFile> partFiles, org.slf4j.Logger logger) {
    super(dcm.getCollectionName(), logger);
    this.config = (FeatureCollectionConfig) dcm.getAuxInfo(FeatureCollectionConfig.AUX_CONFIG);

    this.root = dcm.getRoot();
    this.partIndexFiles = partFiles;
  }

  public Iterable<MCollection> makePartitions(CollectionUpdateType forceCollection) throws IOException {
    return new PartIterator();
  }

  private class PartIterator implements Iterator<MCollection>, Iterable<MCollection> {
    Iterator<MFile> iter = partIndexFiles.iterator();

    @Override
    public Iterator<MCollection> iterator() {
      return this;
    }

    @Override
    public boolean hasNext() {
      return iter.hasNext();
    }

    @Override
    public MCollection next() {
      MFile nextFile = iter.next();

      // try {
        MCollection result = new CollectionSingleIndexFile( nextFile, logger);
        result.putAuxInfo(FeatureCollectionConfig.AUX_CONFIG, config);
        return result;

      //} catch (IOException e) {
      //  logger.error("PartitionManagerFromList failed on "+nextFile.getPath(), e);
     //   throw new RuntimeException(e);
     // }
    }

    @Override
    public void remove() {
    }
  }

  @Override
  public void close() { }

  @Override
  public Iterable<MFile> getFilesSorted() throws IOException {
    return null;
  }

  @Override
  public CloseableIterator<MFile> getFileIterator() throws IOException {
    return null;
  }

  /////////////////////////////////////////////////////////////
  // partitions can be removed (!)

  public void removePartition( MCollection partition) {
    for (MFile mfile : partIndexFiles) {
      if (mfile.getName().equalsIgnoreCase(partition.getCollectionName())) {
        List<MFile> part = new ArrayList<>(partIndexFiles);
        part.remove(mfile);
        partIndexFiles = part;
        return;
      }
    }
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.iosp.cinrad;

import ucar.unidata.io.RandomAccessFile;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.DiskCache;
import ucar.nc2.NetcdfFile;
import ucar.nc2.iosp.nexrad2.NexradStationDB;

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

import ucar.unidata.io.bzip2.CBZip2InputStream;
import ucar.unidata.io.bzip2.BZip2ReadException;

/**
 * This class reads a CINRAD level II data file.
 * It can handle NCDC archives (ARCHIVE2), as well as CRAFT/IDD compressed files (AR2V0001).
 *

 * Adapted with permission from the Java Iras software developed by David Priegnitz at NSSL.

 *

 * Documentation on Archive Level II data format can be found at:
 *
 * http://www.ncdc.noaa.gov/oa/radar/leveliidoc.html
 *
 * @author caron
 * @author David Priegnitz
 */
public class Cinrad2VolumeScan {

  // data formats
  static public final String ARCHIVE2 = "ARCHIVE2";
  static public final String AR2V0001 = "AR2V0001";

  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Cinrad2VolumeScan.class);
  ////////////////////////////////////////////////////////////////////////////////////

  // Data file
  RandomAccessFile raf;

  private String dataFormat = null; // ARCHIVE2 or AR2V0001
  private String volumeNo = null;  // volume number (1 to 999)
  private int title_julianDay; // days since 1/1/70
  private int title_msecs; // milliseconds since midnight
  private String stationId; // 4 letter station assigned by ICAO
  private NexradStationDB.Station station; // from lookup table, may be null
  private Cinrad2Record first, last;

  private int vcp = 0; // Volume coverage pattern
  private int max_radials = 0;
  private int min_radials = Integer.MAX_VALUE;
  private int dopplarResolution;
  private boolean hasDifferentDopplarResolutions;

  // List of List of Cinrad2Record
  private ArrayList reflectivityGroups, dopplerGroups;

  private boolean showMessages = false, showData = false, debugScans = false, debugGroups2 = false, debugRadials = false;

  Cinrad2VolumeScan(RandomAccessFile orgRaf, CancelTask cancelTask) throws IOException {
    this.raf = orgRaf;

    boolean debug = log.isDebugEnabled();
    if (debug)
      log.debug("Cinrad2VolumeScan on " + raf.getLocation());

    raf.seek(0);
    raf.order(RandomAccessFile.LITTLE_ENDIAN); //.BIG_ENDIAN);
    // try to get it from the filename LOOK
    String loc = raf.getLocation();
    stationId = getStationID(loc);
    // volume scan header
    dataFormat = raf.readString(8);
    raf.skipBytes(1);
    volumeNo = raf.readString(3);
    title_julianDay = raf.readInt(); // since 1/1/70
    title_msecs = raf.readInt();
    //stationId = raf.readString(4).trim(); // only in AR2V0001
    if (debug) log.debug(" dataFormat= " + dataFormat + " stationId= " + stationId);

    if (stationId.length() == 0) {
      // try to get it from the filename LOOK
      stationId = null;
    }

    // try to find the station
    if (stationId != null) {
      station = NexradStationDB.get("K" + stationId);
      dataFormat = "CINRAD-SA";
    }

    //   if(station == null) {
    //        station = new NexradStationDB.Station();
    //     stationId = "CHGZ";
    //    station.id = "CHGZ";
    //    station.name = "CHINA, GuanZhou";
    //    station.lat = parseDegree("23:0:14");
    //    station.lon = parseDegree("113:21:18");
    //   station.elev = Double.parseDouble("180.3");
    //    dataFormat = "CINRAD-SA";
    //    }

    //see if we have to uncompress
    if (dataFormat.equals(AR2V0001)) {
      raf.skipBytes(4);
      String BZ = raf.readString(2);
      if (BZ.equals("BZ")) {
        RandomAccessFile uraf = null;
        File uncompressedFile = DiskCache.getFileStandardPolicy(raf.getLocation() + ".uncompress");
        if (uncompressedFile.exists()) {
          uraf = ucar.unidata.io.RandomAccessFile.acquire(uncompressedFile.getPath());
        } else {
          // nope, gotta uncompress it
          try {
            uraf = uncompress(raf, uncompressedFile.getPath(), debug);
            uraf.flush();
          } catch (IOException e) {
            if (uraf != null) uraf.close();
            throw e;
          }

          if (debug) log.debug("flushed uncompressed file= " + uncompressedFile.getPath());
        }
        // switch to uncompressed file
        raf.close();
        raf = uraf;
        raf.order(RandomAccessFile.BIG_ENDIAN);
      }

      raf.seek(Cinrad2Record.FILE_HEADER_SIZE);
    }

    ArrayList reflectivity = new ArrayList();
    ArrayList doppler = new ArrayList();

    int recno = 0;
    int sweepN = 1;
    int [] recordNum = null;
    int sums = 0;
    while (true) {

      Cinrad2Record r = Cinrad2Record.factory(raf, recno++);
      if (r == null) break;

      // skip non-data messages
      if (r.message_type != 1) {
        if (showMessages) r.dumpMessage(System.out, null);
        continue;
      }

      if(recno == 1 && Cinrad2IOServiceProvider.isCC20){
        sweepN = r.sweepN;
        recordNum = r.recordNum;
        //sums = Arrays.stream(recordNum).sum();
        for (int i = 0; i < recordNum.length; i++) {
          sums = sums + recordNum[i];
        }
      }
      if (showData) r.dump2(System.out);

      /* skip bad
      if (!r.checkOk()) {
        r.dump(System.out);
        continue;
      } */

      // some global params
      if (vcp == 0) vcp = r.vcp;
      if (first == null) first = r;
      last = r;

      if (!r.checkOk()) {
        continue;
      }
      if(Cinrad2IOServiceProvider.isCC20 && recno > sums)
        continue;
      if (r.hasReflectData)
        reflectivity.add(r);
      if (r.hasDopplerData)
        doppler.add(r);

      if ((cancelTask != null) && cancelTask.isCancel()) return;
    }

    if(Cinrad2IOServiceProvider.isCC20){
      Iterator itr = reflectivity.iterator();
      for(int i = 0; i< sweepN; i++){
        for(int j = 0; j < recordNum[i]; j++){
          if(itr.hasNext()) {
            Cinrad2Record r = (Cinrad2Record) itr.next();
            r.radial_num = (short) (j + 1);
            r.elevation_num = (short) (i);
          }
        }
      }

    }
    if (debugRadials) System.out.println(" reflect ok= " + reflectivity.size() + " doppler ok= " + doppler.size());

    reflectivityGroups = sortScans("reflect", reflectivity);
    dopplerGroups = sortScans("doppler", doppler);
  }

  public String getStationID(String location) {
    String stationID;
    // posFirst: last '/' if it exists
    int posFirst = location.lastIndexOf('/') + 1;
    if (posFirst < 0) posFirst = 0;
    stationID = location.substring(posFirst, posFirst + 4);
    return stationID;
  }

  private static double parseDegree(String s) {
    StringTokenizer stoke = new StringTokenizer(s, ":");
    String degS = stoke.nextToken();
    String minS = stoke.nextToken();
    String secS = stoke.nextToken();

    try {
      double deg = Double.parseDouble(degS);
      double min = Double.parseDouble(minS);
      double sec = Double.parseDouble(secS);
      if (deg < 0)
        return deg - min / 60 - sec / 3600;
      else
        return deg + min / 60 + sec / 3600;
    } catch (NumberFormatException e) {
      e.printStackTrace();
    }
    return 0.0;
  }

  private ArrayList sortScans(String name, List scans) {

    // now group by elevation_num
    HashMap groupHash = new HashMap(600);
    for (int i = 0; i < scans.size(); i++) {
      Cinrad2Record record = (Cinrad2Record) scans.get(i);
      Integer groupNo = new Integer(record.elevation_num);

      ArrayList group = (ArrayList) groupHash.get(groupNo);
      if (null == group) {
        group = new ArrayList();
        groupHash.put(groupNo, group);
      }

      group.add(record);
    }

    // sort the groups by elevation_num
    ArrayList groups = new ArrayList(groupHash.values());
    Collections.sort(groups, new GroupComparator());

    // use the maximum radials
    for (int i = 0; i < groups.size(); i++) {
      ArrayList group = (ArrayList) groups.get(i);
      max_radials = Math.max(max_radials, group.size());
      min_radials = Math.min(min_radials, group.size());
      testScan(name, group);
    }
    if (debugRadials) {
      System.out.println(name + " min_radials= " + min_radials + " max_radials= " + max_radials);
      for (int i = 0; i < groups.size(); i++) {
        ArrayList group = (ArrayList) groups.get(i);
        Cinrad2Record lastr = (Cinrad2Record) group.get(0);
        for (int j = 1; j < group.size(); j++) {
          Cinrad2Record r = (Cinrad2Record) group.get(j);
          if (r.data_msecs < lastr.data_msecs)
            System.out.println(" out of order " + j);
          lastr = r;
        }
      }
    }

    testVariable(name, groups);
    if (debugScans) System.out.println("-----------------------------");

    return groups;
  }

  public int getMaxRadials() {
    return max_radials;
  }

  public int getMinRadials() {
    return min_radials;
  }

  public int getDopplarResolution() {
    return dopplarResolution;
  }

  public boolean hasDifferentDopplarResolutions() {
    return hasDifferentDopplarResolutions;
  }

  // do we have same characteristics for all records in a scan?

  private boolean testScan(String name, ArrayList group) {
    int MAX_RADIAL = max_radials +1;
    int[] radial = new int[MAX_RADIAL];
    int datatype = name.equals("reflect") ? Cinrad2Record.REFLECTIVITY : Cinrad2Record.VELOCITY_HI;
    Cinrad2Record first = (Cinrad2Record) group.get(0);

    int n = group.size();
    if (debugScans) {
      boolean hasBoth = first.hasDopplerData && first.hasReflectData;
      System.out.println(name + " " + first + " has " + n + " radials resolution= " + first.resolution + " has both = " + hasBoth);
    }

    boolean ok = true;
    double sum = 0.0;
    double sum2 = 0.0;

    for (int i = 0; i < MAX_RADIAL; i++)
      radial[i] = 0;

    for (int i = 0; i < group.size(); i++) {
      Cinrad2Record r = (Cinrad2Record) group.get(i);

      /* this appears to be common - seems to be ok, we put missing values in
      if (r.getGateCount(datatype) != first.getGateCount(datatype)) {
        log.error(raf.getLocation()+" different number of gates ("+r.getGateCount(datatype)+
                "!="+first.getGateCount(datatype)+") in record "+name+ " "+r);
        ok = false;
      } */

      if (r.getGateSize(datatype) != first.getGateSize(datatype)) {
        log.warn(raf.getLocation() + " different gate size (" + r.getGateSize(datatype) + ") in record " + name + " " + r);
        ok = false;
      }
      if (r.getGateStart(datatype) != first.getGateStart(datatype)) {
        log.warn(raf.getLocation() + " different gate start (" + r.getGateStart(datatype) + ") in record " + name + " " + r);
        ok = false;
      }
      if (r.resolution != first.resolution) {
        log.warn(raf.getLocation() + " different resolution (" + r.resolution + ") in record " + name + " " + r);
        ok = false;
      }

      if ((r.radial_num < 0) || (r.radial_num > MAX_RADIAL)) {
        log.info(raf.getLocation() + " radial out of range= " + r.radial_num + " in record " + name + " " + r);
        continue;
      }
      if (radial[r.radial_num] > 0) {
        log.warn(raf.getLocation() + " duplicate radial = " + r.radial_num + " in record " + name + " " + r);
        ok = false;
      }
      radial[r.radial_num] = r.recno + 1;

      sum += r.getElevation();
      sum2 += r.getElevation() * r.getElevation();
      // System.out.println("  elev="+r.getElevation()+" azi="+r.getAzimuth());
    }

    for (int i = 1; i < radial.length; i++) {
      if (0 == radial[i]) {
        if (n != (i - 1)) {
          log.warn(" missing radial(s)");
          ok = false;
        }
        break;
      }
    }

    // double avg = sum / n;
    // double sd = Math.sqrt((n * sum2 - sum * sum) / (n * (n - 1)));
    // System.out.println(" avg elev="+avg+" std.dev="+sd);

    return ok;
  }

  // do we have same characteristics for all groups in a variable?
  private boolean testVariable(String name, List scans) {
    int datatype = name.equals("reflect") ? Cinrad2Record.REFLECTIVITY : Cinrad2Record.VELOCITY_HI;
    if (scans.size() == 0) {
      log.warn(" No data for = " + name);
      return false;
    }

    boolean ok = true;
    List firstScan = (List) scans.get(0);
    Cinrad2Record firstRecord = (Cinrad2Record) firstScan.get(0);
    dopplarResolution = firstRecord.resolution;

    if (debugGroups2)
      System.out.println("Group " + Cinrad2Record.getDatatypeName(datatype) + " ngates = " + firstRecord.getGateCount(datatype) +
              " start = " + firstRecord.getGateStart(datatype) + " size = " + firstRecord.getGateSize(datatype));

    for (int i = 1; i < scans.size(); i++) {
      List scan = (List) scans.get(i);
      Cinrad2Record record = (Cinrad2Record) scan.get(0);

      if ((datatype == Cinrad2Record.VELOCITY_HI) && (record.resolution != firstRecord.resolution)) { // do all velocity resolutions match ??
        log.warn(name + " scan " + i + " diff resolutions = " + record.resolution + ", " + firstRecord.resolution +
                " elev= " + record.elevation_num + " " + record.getElevation());
        ok = false;
        hasDifferentDopplarResolutions = true;
      }

      if (record.getGateSize(datatype) != firstRecord.getGateSize(datatype)) {
        log.warn(name + " scan " + i + " diff gates size = " + record.getGateSize(datatype) + " " + firstRecord.getGateSize(datatype) +
                " elev= " + record.elevation_num + " " + record.getElevation());
        ok = false;

      } else if (debugGroups2)
        System.out.println(" ok gates size elev= " + record.elevation_num + " " + record.getElevation());

      if (record.getGateStart(datatype) != firstRecord.getGateStart(datatype)) {
        log.warn(name + " scan " + i + " diff gates start = " + record.getGateStart(datatype) + " " + firstRecord.getGateStart(datatype) +
                " elev= " + record.elevation_num + " " + record.getElevation());
        ok = false;

      } else if (debugGroups2)
        System.out.println(" ok gates start elev= " + record.elevation_num + " " + record.getElevation());
    }

    return ok;
  }

  /**
   * Get Reflectivity Groups
   * Groups are all the records for a variable and elevation_num;
   *
   * @return List of type List of type Cinrad2Record
   */
  public List getReflectivityGroups() {
    return reflectivityGroups;
  }

  /**
   * Get Velocity Groups
   * Groups are all the records for a variable and elevation_num;
   *
   * @return List of type List of type Cinrad2Record
   */
  public List getVelocityGroups() {
    return dopplerGroups;
  }

  private static class GroupComparator implements Comparator {

    public int compare(Object o1, Object o2) {
      List group1 = (List) o1;
      List group2 = (List) o2;
      Cinrad2Record record1 = (Cinrad2Record) group1.get(0);
      Cinrad2Record record2 = (Cinrad2Record) group2.get(0);

      //if (record1.elevation_num != record2.elevation_num)
      return record1.elevation_num - record2.elevation_num;
      //return record1.cut - record2.cut;
    }
  }

  /**
   * Get data format (ARCHIVE2, AR2V0001) for this file.
   */
  public String getDataFormat() {
    return dataFormat;
  }

  /**
   * Get the starting Julian day for this volume
   *
   * @return days since 1/1/70.
   */
  public int getTitleJulianDays() {
    return title_julianDay;
  }

  /**
   * Get the starting time in seconds since midnight.
   *
   * @return Generation time of data in milliseconds of day past  midnight (UTC).
   */
  public int getTitleMsecs() {
    return title_msecs;
  }

  /**
   * Get the Volume Coverage Pattern number for this data.
   *
   * @return VCP
   * @see Cinrad2Record#getVolumeCoveragePatternName
   */
  public int getVCP() {
    return vcp;
  }

  /**
   * Get the 4-char station ID for this data
   *
   * @return station ID (may be null)
   */

  public String getStationId() {
    return stationId;
  }

  public String getStationName() {
    return station == null ? "unknown" : station.name;
  }

  public double getStationLatitude() {
    return station == null ? 0.0 : station.lat;
  }

  public double getStationLongitude() {
    return station == null ? 0.0 : station.lon;
  }

  public double getStationElevation() {
    return station == null ? 0.0 : station.elev;
  }

  public Date getStartDate() {
    return first.getDate();
  }

  public Date getEndDate() {
    return last.getDate();
  }

  /**
   * Write equivilent uncompressed version of the file.
   *
   * @param raf2      file to uncompress
   * @param ufilename write to this file
   * @return raf of uncompressed file
   * @throws IOException
   */
  private RandomAccessFile uncompress(RandomAccessFile raf2, String ufilename, boolean debug) throws IOException {
    raf2.seek(0);
    byte[] header = new byte[Cinrad2Record.FILE_HEADER_SIZE];
    int bytesRead = raf2.read(header);
    if (bytesRead != header.length) {
      throw new IOException("Error reading CINRAD header -- got " +
              bytesRead + " rather than" + header.length);
    }
    RandomAccessFile dout2 = new RandomAccessFile(ufilename, "rw");

    boolean eof = false;
    int numCompBytes;
    byte[] ubuff = new byte[40000];
    byte[] obuff = new byte[40000];
    try {
      dout2.write(header);
      CBZip2InputStream cbzip2 = new CBZip2InputStream();
      while (!eof) {

        try {
          numCompBytes = raf2.readInt();
          if (numCompBytes == -1) {
            if (debug) log.debug("  done: numCompBytes=-1 ");
            break;
          }
        } catch (EOFException ee) {
          if (debug) log.debug("  got EOFException ");
          break; // assume this is ok
        }

        if (debug) {
          log.debug("reading compressed bytes " + numCompBytes + " input starts at " + raf2.getFilePointer() + "; output starts at " + dout2.getFilePointer());
        }
        /*
        * For some stupid reason, the last block seems to
        * have the number of bytes negated.  So, we just
        * assume that any negative number (other than -1)
        * is the last block and go on our merry little way.
        */
        if (numCompBytes < 0) {
          if (debug) log.debug("last block?" + numCompBytes);
          numCompBytes = -numCompBytes;
          eof = true;
        }
        byte[] buf = new byte[numCompBytes];
        raf2.readFully(buf);
        ByteArrayInputStream bis = new ByteArrayInputStream(buf, 2, numCompBytes - 2);

        //CBZip2InputStream cbzip2 = new CBZip2InputStream(bis);
        cbzip2.setStream(bis);
        int total = 0;
        int nread;
        /*
        while ((nread = cbzip2.read(ubuff)) != -1) {
          dout2.write(ubuff, 0, nread);
          total += nread;
        }
        */
        try {
          while ((nread = cbzip2.read(ubuff)) != -1) {
            if (total + nread > obuff.length) {
              byte[] temp = obuff;
              obuff = new byte[temp.length * 2];
              System.arraycopy(temp, 0, obuff, 0, temp.length);
            }
            System.arraycopy(ubuff, 0, obuff, total, nread);
            total += nread;
          }
          if (obuff.length >= 0) dout2.write(obuff, 0, total);
        } catch (BZip2ReadException ioe) {
          log.debug("Cinrad2IOSP.uncompress ", ioe);
        }
        float nrecords = (float) (total / 2432.0);
        if (debug)
          log.debug("  unpacked " + total + " num bytes " + nrecords + " records; ouput ends at " + dout2.getFilePointer());
      }
      dout2.flush();
    } catch (EOFException e) {
      e.printStackTrace();
    } catch (Exception e) {
      dout2.close();
      throw e;
    }

    return dout2;
  }

  // check if compressed file seems ok
  // LOOK While the IOSP seems to read files fine, this function seems terribly
  // broken on the data in our cdmUnitTests/formats/cinrad directory.
  static public long testValid(String ufilename) throws IOException {
    boolean lookForHeader = false;

    // gotta make it
    try (RandomAccessFile raf = new RandomAccessFile(ufilename, "r")) {
      raf.order(RandomAccessFile.LITTLE_ENDIAN); //.BIG_ENDIAN);
      raf.seek(0);
      String test = raf.readString(8);
      if (test.equals(Cinrad2VolumeScan.ARCHIVE2) || test.equals
              (Cinrad2VolumeScan.AR2V0001)) {
        System.out.println("--Good header= " + test);
        raf.seek(24);
      } else {
        System.out.println("--No header ");
        lookForHeader = true;
        raf.seek(0);
      }

      boolean eof = false;
      int numCompBytes;

      while (!eof) {

        if (lookForHeader) {
          test = raf.readString(8);
          if (test.equals(Cinrad2VolumeScan.ARCHIVE2) || test.equals(Cinrad2VolumeScan.AR2V0001)) {
            System.out.println("  found header= " + test);
            raf.skipBytes(16);
            lookForHeader = false;
          } else {
            raf.skipBytes(-8);
          }
        }

        try {
          numCompBytes = raf.readInt();
          if (numCompBytes == -1) {
            System.out.println("\n--done: numCompBytes=-1 ");
            break;
          }
        } catch (EOFException ee) {
          System.out.println("\n--got EOFException ");
          break; // assume this is ok
        }

        System.out.print(" " + numCompBytes + ",");
        if (numCompBytes < 0) {
          System.out.println("\n--last block " + numCompBytes);
          numCompBytes = -numCompBytes;
          if (!lookForHeader) eof = true;
        }

        raf.skipBytes(numCompBytes);
      }
      return raf.getFilePointer();
    } catch (EOFException e) {
      e.printStackTrace();
    }
    return 0;
  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.iosp.grid;

import ucar.ma2.*;

import ucar.nc2.*;
//import ucar.nc2.dt.fmr.FmrcCoordSys;
import ucar.nc2.iosp.AbstractIOServiceProvider;
import ucar.nc2.util.CancelTask;

import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;

/**
 * Superclass for Gempak grid, MciDAS grid
 *
 * @author IDV Development Team
 */
public abstract class GridServiceProvider extends AbstractIOServiceProvider {

  public enum IndexExtendMode {
    /**
     * if data file changes, completely rewrite the index
     */
    rewrite,

    /**
     * if data file changes, assume its been extended, so extend the index
     */
    extendwrite,

    /**
     * if index file exists, use it as is
     */
    readonly
  }

  // these defaults are for clients, TDS sets these explicitly
  static protected IndexExtendMode indexFileModeOnOpen = IndexExtendMode.rewrite; // default is to rewrite
  static protected IndexExtendMode indexFileModeOnSync = IndexExtendMode.extendwrite; // default is to extend

  static protected boolean addLatLon = false; // add lat/lon coordinates for strict CF compliance LOOK should not be static !
  static protected boolean forceNewIndex = false; // force that a new index file is written - for debugging
  static protected boolean alwaysInCache = false;

  /**
   * debug flags
   */
  public static boolean debugOpen = false,
          debugMissing = false,
          debugMissingDetails = false,
          debugProj = false,
          debugTiming = false,
          debugVert = false;

  /**
   * Set whether to force new index or not
   *
   * @param b true to use
   */
  static public void forceNewIndex(boolean b) {
    forceNewIndex = b;
  }

  /**
   * Set the debug flags
   *
   * @param debugFlag debug flags
   */
  static public void setDebugFlags(ucar.nc2.util.DebugFlags debugFlag) {
    debugOpen = debugFlag.isSet("Grid/open");
    debugMissing = debugFlag.isSet("Grid/missing");
    debugMissingDetails = debugFlag.isSet("Grid/missingDetails");
    debugProj = debugFlag.isSet("Grid/projection");
    debugVert = debugFlag.isSet("Grid/vertical");
    debugTiming = debugFlag.isSet("Grid/timing");
  }

  /**
   * This controls what happens when a GRIB file is opened, and the data file has changed since the index was written.
   *

   *
IndexExtendMode.extendwrite: when GRIB file length increases, extend the index. This is the case when the file
   * is being appended to, as new data arrives.
   *
IndexExtendMode.rewrite: when GRIB file length changes, rewrite the index. This is the safest thing to do,
   * at the expense of performance.
   *
IndexExtendMode.readonly: never modify an existing index, just use it. However, if there is no index, created one
   *

   *
   * @param mode IndexExtendMode when file is opened
   */
  static public void setIndexFileModeOnOpen(IndexExtendMode mode) {
    indexFileModeOnOpen = mode;
  }

  /**
   * This controls what happens when sync() is called on a GRIB file. The main use of sync() is when you are using
   * NetcdfFile object caching. Before NetcdfFile is returned from a cache hit, sync() is called on it.
   * Default is IndexExtendMode.extend.
   *
   * @param mode IndexExtendMode when sync() is called. Same meaning as setIndexExtendMode(IndexExtendMode mode)
   */
  /**
   * This controls what happens when a GRIB file is synced (usually from FileCache), and the data or index file has changed
   *  since the file was placed in the cache.
   *

   *
IndexExtendMode.extendwrite: when GRIB file or index length increases, extend the index. If file or index length
   *   decreases, rewrite it.
   *
IndexExtendMode.rewrite: when GRIB file length changes, rewrite the index.
   *
IndexExtendMode.readonly: never modify an existing index, just use it. However, if there is no index, created one
   *

   *
   * @param mode IndexExtendMode when file is opened
   */
  static public void setIndexFileModeOnSync(IndexExtendMode mode) {
    indexFileModeOnSync = mode;
  }

  // backwards compatible with old API
  /**
   * Set how indexes are used for both open and sync
   *
   * @param b if true, set modes to IndexExtendMode.extendwrite, else IndexExtendMode.readonly
   * @deprecated use setIndexFileModeOnSync and setIndexFileModeOnOpen
   */
  static public void setExtendIndex(boolean b) {
    indexFileModeOnOpen = b ? IndexExtendMode.extendwrite : IndexExtendMode.readonly;
    indexFileModeOnSync = b ? IndexExtendMode.extendwrite : IndexExtendMode.readonly;
  }

  /**
   * Set disk cache policy for index files.
   * Default = false, meaning try to write index files in same directory as grib file.
   * True means always use the DiskCache area. TDS sets this to true, so it wont interfere with external indexer.
   *
   * @param b set to this value
   */
  static public void setIndexAlwaysInCache(boolean b) {
    alwaysInCache = b;
  }

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

  /**
   * Use the given index to fill the NetcdfFile object with attributes and variables.
   *
   * @param index      GridIndex to use
   * @param cancelTask cancel task
   * @throws IOException problem reading the file
   */
  protected abstract void open(GridIndex index, CancelTask cancelTask) throws IOException;

  /**
   * Open the service provider for reading.
   *
   * @param raf        file to read from
   * @param ncfile     netCDF file we are writing to (memory)
   * @param cancelTask task for cancelling
   * @throws IOException problem reading file
   */
  @Override
  public void open(RandomAccessFile raf, NetcdfFile ncfile, CancelTask cancelTask) throws IOException {
    super.open(raf, ncfile, cancelTask);
  }

  /**
   * Read the data for the variable
   *
   * @param v2      Variable to read
   * @param section section infomation
   * @return Array of data
   * @throws IOException           problem reading from file
   * @throws InvalidRangeException invalid Range
   */
  @Override
  public Array readData(Variable v2, Section section) throws IOException, InvalidRangeException {
    long start = System.currentTimeMillis();

    Array dataArray = Array.factory(DataType.FLOAT, section.getShape());
    GridVariable pv = (GridVariable) v2.getSPobject();

    // Canonical ordering is ens, time, level, lat, lon
    int rangeIdx = 0;
    Range ensRange = pv.hasEnsemble() ? section.getRange(rangeIdx++) : new Range( 0, 0 );
    Range timeRange = (section.getRank() > 2) ? section.getRange(rangeIdx++) : new Range( 0, 0 );
    Range levRange = pv.hasVert() ? section.getRange(rangeIdx++) : new Range( 0, 0 );
    Range yRange = section.getRange(rangeIdx++);
    Range xRange = section.getRange(rangeIdx);

    IndexIterator ii = dataArray.getIndexIterator();

    for (int ensIdx : ensRange) {
      for (int timeIdx : timeRange) {
        for (int levelIdx : levRange) {
          readXY(v2, ensIdx, timeIdx, levelIdx, yRange, xRange, ii);
        }
      }
    }

    if (debugTiming) {
      long took = System.currentTimeMillis() - start;
      System.out.println("  read data took=" + took + " msec ");
    }

    return dataArray;
  }

  /**
   * read one YX array
   *
   * @param v2      variable to put the data into
   * @param ensIdx  ensemble index
   * @param timeIdx time index
   * @param levIdx  level index
   * @param yRange  x range
   * @param xRange  y range
   * @param ii      index iterator
   * @throws IOException           problem reading the file
   * @throws InvalidRangeException invalid range
   */
  private void readXY(Variable v2, int ensIdx, int timeIdx, int levIdx, Range yRange, Range xRange, IndexIterator ii)
          throws IOException, InvalidRangeException {

    GridVariable pv = (GridVariable) v2.getSPobject();
    GridHorizCoordSys hsys = pv.getHorizCoordSys();
    int nx = hsys.getNx();
    GridRecord record = pv.findRecord(ensIdx, timeIdx, levIdx);

    if (record == null) {
      Attribute att = v2.findAttribute("missing_value");
      float missing_value = (att == null) ? -9999.0f : att.getNumericValue().floatValue();

      int xyCount = yRange.length() * xRange.length();
      for (int j = 0; j < xyCount; j++) {
        ii.setFloatNext(missing_value);
      }
      return;
    }

    // otherwise read it
    float[] data = _readData(record);
    if (data == null) {
      _readData(record); // debug
      return;
    }

    // LOOK can improve with System.copy ??
    for (int y : yRange) {
      for (int x : xRange) {
        int index = y * nx + x;
        ii.setFloatNext(data[index]);
      }
    }
  }

  /**
   * Is this XY level missing?
   *
   * @param v2      Variable
   * @param timeIdx time index
   * @param ensIdx  ensemble index
   * @param levIdx  level index
   * @return true if missing
   * @throws InvalidRangeException invalid range
   */
  public boolean isMissingXY(Variable v2, int timeIdx, int ensIdx, int levIdx) throws InvalidRangeException {
    GridVariable pv = (GridVariable) v2.getSPobject();
    if ((timeIdx < 0) || (timeIdx >= pv.getNTimes())) {
      throw new InvalidRangeException("timeIdx=" + timeIdx);
    }
    if ((levIdx < 0) || (levIdx >= pv.getVertNlevels())) {
      throw new InvalidRangeException("levIdx=" + levIdx);
    }
    if ((ensIdx < 0) || (ensIdx >= pv.getNEnsembles())) {
      throw new InvalidRangeException("ensIdx=" + ensIdx);
    }
    return (null == pv.findRecord(ensIdx, timeIdx, levIdx));
  }

  /**
   * Read the data for this GridRecord
   *
   * @param gr grid identifier
   * @return the data (or null)
   * @throws IOException problem reading the data
   */
  protected abstract float[] _readData(GridRecord gr) throws IOException;

}


/* Copyright 2009, UCAR/Unidata and OPeNDAP, Inc.
   See the LICENCE file for more information. */

package dap4.servlet;

import dap4.core.data.ChecksumMode;
import dap4.core.util.DapException;
import dap4.core.util.DapUtil;
import dap4.core.util.ResponseFormat;
import dap4.dap4lib.Dap4Util;
import dap4.dap4lib.DapLog;
import dap4.dap4lib.RequestMode;
import dap4.dap4lib.XURI;
import ucar.httpservices.HTTPUtil;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;

/**
 * User requests get cached here so that downstream code can access
 * the details of the request information.
 *

 * Modified by Heimbigner for DAP4.
 *
 * @author Nathan Potter
 * @author Dennis Heimbigner
 */

public class DapRequest
{
    //////////////////////////////////////////////////
    // Constants

    static final boolean DEBUG = false;

    static public final String WEBINFPATH = "/WEB-INF";
    static public final String RESOURCEDIRNAME = "resources";

    static public final String CONSTRAINTTAG = "dap4.ce";

    static public final ChecksumMode DEFAULTCSUM = ChecksumMode.DAP;

    //////////////////////////////////////////////////
    // Instance variables

    protected HttpServletRequest request = null;
    protected HttpServletResponse response = null;

    protected String url = null;  // without any query  and as with any modified dataset path
    protected String querystring = null;
    protected String server = null; // scheme + host + port
    protected String controllerpath = null; // scheme + host + port + prefix of path thru servlet Notes
    protected String datasetpath = null; // past controller path; char(0) != '/'

    protected RequestMode mode = null; // .dmr, .dap, or .dsr
    protected ResponseFormat format = null; // e.g. .xml when given .dmr.xml

    protected Map<String, String> queries = new HashMap<String, String>();
    protected DapController controller = null;

    protected ByteOrder order = ByteOrder.nativeOrder();
    protected ChecksumMode checksummode = null;
    protected String resourceroot = null;

    protected ServletContext servletcontext = null;

    //////////////////////////////////////////////////
    // Constructor(s)

    public DapRequest(DapController controller,
                      HttpServletRequest request,
                      HttpServletResponse response)
            throws DapException
    {
        this.controller = controller;
        this.request = request;
        this.response = response;
        this.servletcontext = request.getServletContext();

        // Figure out the absolute path to our resources directory
        this.resourceroot = (String) this.request.getAttribute("RESOURCEDIR");
        if(this.resourceroot == null && this.servletcontext != null) {
            try {
                URL url = this.servletcontext.getResource(WEBINFPATH);
                if(url == null)
                    this.resourceroot = null;
                else {
                    if(!url.getProtocol().equals("file"))
                        throw new DapException("Cannot locate resource root");
                    this.resourceroot = DapUtil.canonicalpath(url.getFile());
                }
            } catch (MalformedURLException e) {
                this.resourceroot = null;
            }
            if(this.resourceroot == null)
                throw new DapException("Cannot locate resource root");
        }
        try {
            parse();
        } catch (IOException ioe) {
            throw new DapException(ioe);
        }
    }

    //////////////////////////////////////////////////
    // Request path parsing

    /**
     * The goal of parse() is to extract info
     * from the underlying HttpRequest and cache it
     * in this object.
     *

     * In particular, the incoming URL needs to be decomposed
     * into multiple pieces. Certain assumptions are made:
     * 1. every incoming url is of the form
     * (a) http(s)://host:port/d4ts/
     * or
     * (b) http(s)://host:port/d4ts/<datasetpath>?query
     * Case a indicates that the front page is to be returned.
     * Case b indicates a request for a dataset (or dsr), and its
     * value is determined by its extensions. The query may be absent.
     * We want to extract the following pieces.
     * 1. (In URI parlance) The scheme plus the authority:
     * http://host:port
     * 3. The return type: depending on the last extension (e.g. ".txt").
     * 4. The requested value: depending on the next to last extension (e.g. ".dap").
     * 5. The suffix path specifying the actual dataset: datasetpath
     * with return and request type extensions removed.
     * 6. The url path = servletpath + datasetpath.
     * 7. The query part.
     */

    protected void
    parse()
            throws IOException
    {
        this.url = request.getRequestURL().toString(); // does not include query
        // The mock servlet code does not construct a query string,
        // so if we are doing testing, construct from parametermap.
        if(DapController.TESTING) {
            this.querystring = makeQueryString(this.request);
        } else {
            this.querystring = request.getQueryString();    // raw (undecoded)
        }

        XURI xuri;
        try {
            xuri = new XURI(this.url).parseQuery(this.querystring);
        } catch (URISyntaxException e) {
            throw new IOException(e);
        }
        // Now, construct various items
        StringBuilder buf = new StringBuilder();
        buf.append(request.getScheme());
        buf.append("://");
        buf.append(request.getServerName());
        int port = request.getServerPort();
        if(port > 0) {
            buf.append(":");
            buf.append(port);
        }
        this.server = buf.toString();

        // There appears to be some inconsistency in how the url path is divided up
        // depending on if this is a spring controller vs a raw servlet.
        // Try to canonicalize so that context path always ends with id
        // and servletpath does not start with it.
        String id = controller.getServletID();
        String sp = DapUtil.canonicalpath(request.getServletPath());
        String cp = DapUtil.canonicalpath(request.getContextPath());
        if(!cp.endsWith(id)) // probably spring ; contextpath does not ends with id
            cp = cp + "/" + id;
        buf.append(cp);
        this.controllerpath = buf.toString();
        sp = HTTPUtil.relpath(sp);
        if(sp.startsWith(id)) // probably spring also
            sp = sp.substring(id.length());

        this.datasetpath = HTTPUtil.relpath(sp);
        this.datasetpath = DapUtil.nullify(this.datasetpath);

        this.mode = null;
        if(this.datasetpath == null) {
            // Presume mode is a capabilities request
            this.mode = RequestMode.CAPABILITIES;
            this.format = ResponseFormat.HTML;
        } else {
            // Decompose path by '.'
            String[] pieces = this.datasetpath.split("[.]");
            // Search backward looking for the mode (dmr or dap)
            // meanwhile capturing the format extension
            int modepos = 0;
            for(int i = pieces.length - 1; i >= 1; i--) {//ignore first piece
                String ext = pieces[i];
                // We assume that the set of response formats does not interset the set of request modes
                RequestMode mode = RequestMode.modeFor(ext);
                ResponseFormat format = ResponseFormat.formatFor(ext);
                if(mode != null) {
                    // Stop here
                    this.mode = mode;
                    modepos = i;
                    break;
                } else if(format != null) {
                    if(this.format != null)
                        throw new DapException("Multiple response formats specified: " + ext)
                                .setCode(HttpServletResponse.SC_BAD_REQUEST);
                    this.format = format;
                }
            }
            // Set the datasetpath to the entire path before the mode defining extension.
            if(modepos > 0)
                this.datasetpath = DapUtil.join(pieces, ".", 0, modepos);
        }

        if(this.mode == null)
            this.mode = RequestMode.DSR;
        if(this.format == null)
            this.format = ResponseFormat.NONE;

        // Parse the query string into a Map
        if(querystring != null && querystring.length() > 0)
            this.queries = xuri.getQueryFields();

        // For testing purposes, get the desired endianness to use with replies
        String p = queryLookup(Dap4Util.DAP4ENDIANTAG);
        if(p != null) {
            Integer oz = DapUtil.stringToInteger(p);
            if(oz == null)
                this.order = ByteOrder.LITTLE_ENDIAN;
            else
                this.order = (oz != 0 ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        }

        // Ditto for no checksum
        p = queryLookup(Dap4Util.DAP4CSUMTAG);
        if(p != null) {
            this.checksummode = ChecksumMode.modeFor(p);
        }
        if(this.checksummode == null)
            this.checksummode = DEFAULTCSUM;

        if(DEBUG) {
            DapLog.debug("DapRequest: controllerpath =" + this.controllerpath);
            DapLog.debug("DapRequest: extension=" + (this.mode == null ? "null" : this.mode.extension()));
            DapLog.debug("DapRequest: datasetpath=" + this.datasetpath);
        }
    }

    //////////////////////////////////////////////////
    // Accessor(s)

    public ByteOrder getOrder()
    {
        return this.order;
    }

    public String getResourceRoot()
    {
        return this.resourceroot;
    }

    public ChecksumMode
    getChecksumMode()
    {
        return this.checksummode;
    }

    public HttpServletRequest getRequest()
    {
        return request;
    }

    public HttpServletResponse getResponse()
    {
        return response;
    }

    public OutputStream getOutputStream()
            throws IOException
    {
        return response.getOutputStream();
    }

    public String getURL()
    {
        return this.url;
    }

    public String getOriginalURL()
    {
        return (this.querystring == null ? this.url
                : this.url + "?" + this.querystring);
    }

    public String getDataset()
    {
        return this.datasetpath;
    }

    public String getServer()
    {
        return this.server;
    }

    public ServletContext getServletContext()
    {
        return this.servletcontext;
    }

    public String getControllerPath()
    {
        return this.controllerpath;
    }

    public String getURLPath()
    {
        return this.controllerpath + (this.datasetpath == null ? "" : this.datasetpath);
    }

    public RequestMode getMode()
    {
        return this.mode;
    }

    public ResponseFormat getFormat()
    {
        return this.format;
    }

    /**
     * Set a request header
     *
     * @param name  the header name
     * @param value the header value
     */
    public void setResponseHeader(String name, String value)
    {
        this.response.setHeader(name, value);
    }

    public String queryLookup(String name)
    {
        return queries.get(name.toLowerCase());
    }

    public Map<String, String>
    getQueries()
    {
        return this.queries;
    }

    public String getResourcePath(String relpath)
            throws IOException
    {
        return controller.getResourcePath(this, relpath);
    }

    public String getDatasetPath()
    {
        return this.datasetpath;
    }

    static String
    makeQueryString(HttpServletRequest req)
    {
        Map<String, String[]> map = req.getParameterMap();
        if(map == null || map.size() == 0) return null;
        StringBuilder q = new StringBuilder();
        for(Map.Entry<String, String[]> entry : map.entrySet()) {
            String[] values = entry.getValue();
            if(values == null || values.length == 0) {
                q.append("&");
                q.append(entry.getKey());
            } else for(int i = 0; i < values.length; i++) {
                q.append("&");
                q.append(entry.getKey());
                q.append("=");
                q.append(values[i]);
            }
        }
        if(q.length() > 0) q.deleteCharAt(0);// leading &
        return q.toString();
    }
}