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

package ucar.nc2.iosp.sigmet;

//~--- non-JDK imports --------------------------------------------------------

import ucar.nc2.Variable;
import ucar.unidata.io.RandomAccessFile;

import java.util.*;

/**
 * @author yuanho
 * @since Apr 7, 2010
 */
public class SigmetVolumeScan {
  String[] data_name = {
          " ", "TotalPower", "Reflectivity", "Velocity", "Width", "DifferentialReflectivity"
  };
  private List<List<Ray>> differentialReflectivityGroups;
  private List<List<Ray>> reflectivityGroups;
  private List<List<Ray>> totalPowerGroups;
  private List<List<Ray>> velocityGroups;
  private List<List<Ray>> widthGroups;
  private List<List<Ray>> timeGroups;
  private int[] num_gates;
  public int[] base_time;
  public short[] year;
  public short[] month;
  public short[] day;
  public Ray firstRay = null;
  public Ray lastRay = null;
  public ucar.unidata.io.RandomAccessFile raf;
  public boolean hasReflectivity = false;
  public boolean hasVelocity = false;
  public boolean hasWidth = false;
  public boolean hasTotalPower = false;
  public boolean hasDifferentialReflectivity = false;
  public boolean hasTime = false;

  /**
   * Read all the values from SIGMET-IRIS file which are necessary to fill in the ncfile.
   *
   * @param raf     ucar.unidata.io.RandomAccessFile corresponds to SIGMET datafile.
   * @param ncfile  an empty NetcdfFile object which will be filled.
   * @param varList ArrayList of Variables of ncfile
   */
  SigmetVolumeScan(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile, ArrayList<Variable> varList)
          throws java.io.IOException {
    final int REC_SIZE = 6144;
    int len = 12288;    // ---- Read from the 3d record----------- 6144*2=12288
    short nrec = 0,
            nsweep = 1,
            nray = 0,
            byteoff = 0;
    int nwords = 0,
            end_words = 0,
            data_read = 0,
            num_zero = 0,
            rays_count = 0,
            nb = 0,
            pos = 0,
            pos_ray_hdr = 0,
            t = 0;
    short a0 = 0,
            a00 = 0,
            dty = 1;
    short beg_az = 0,
            beg_elev = 0,
            end_az = 0,
            end_elev = 0,
            num_bins = 0,
            time_start_sw = 0;
    float az = 0.0f,
            elev = 0.0f,
            d = 0.0f,
            step = 0.0f;
    //     byte      data          = 0;
    boolean beg_rec = true,
            end_rec = true,
            read_ray_hdr = true,
            begin = true;
    int cur_len = len,
            beg = 1,
            kk = 0,
            col = 0,
            nu = 0,
            bt0 = 0,
            bt1 = 0;
    int start_sweep = 1,
            end_sweep = 1,
            start_ray = 1,
            end_ray = 1;

    // Input
    this.raf = raf;
    raf.order(RandomAccessFile.LITTLE_ENDIAN);

    int fileLength = (int) raf.length();
    java.util.Map<String, Number> recHdr = SigmetIOServiceProvider.readRecordsHdr(raf);
    int nparams = recHdr.get("nparams").intValue();    // System.out.println("DO: nparams="+nparams);
    short number_sweeps = recHdr.get("number_sweeps").shortValue();

    // System.out.println("DO: number_sweeps="+number_sweeps);
    short num_rays = recHdr.get("num_rays").shortValue();

    // System.out.println("DO: num_rays="+num_rays);
    int range_1st = recHdr.get("range_first").intValue();
    float range_first = range_1st * 0.01f;
    int stepp = recHdr.get("range_last").intValue();

    // System.out.println("DO: stepp="+stepp);
    float range_last = stepp * 0.01f;
    short bins = recHdr.get("bins").shortValue();

    // System.out.println("DO: bins="+bins);
    // int[]   base_time    = new int[nparams * number_sweeps];

    short[] num_sweep = new short[nparams];
    short[] num_rays_swp = new short[nparams];
    short[] indx_1ray = new short[nparams];
    short[] num_rays_act = new short[nparams];
    short[] angl_swp = new short[nparams];
    short[] bin_len = new short[nparams];
    short[] data_type = new short[nparams];
    // float[] dd           = new float[bins];
    num_gates = new int[number_sweeps];
    end_sweep = (int) number_sweeps;
    end_ray = (int) num_rays;
    base_time = new int[nparams * number_sweeps];
    year = new short[nparams * number_sweeps];
    month = new short[nparams * number_sweeps];
    day = new short[nparams * number_sweeps];
    // Array of Ray objects is 2D. Number of columns=number of rays
    // Number of raws = number of types of data if number_sweeps=1,
    // or number of raws = number_sweeps
    List<Ray> totalPower = new ArrayList<Ray>();
    List<Ray> velocity = new ArrayList<Ray>();
    List<Ray> reflectivity = new ArrayList<Ray>();
    List<Ray> width = new ArrayList<Ray>();
    List<Ray> diffReflectivity = new ArrayList<Ray>();
    List<Ray> time = new ArrayList<Ray>();
    int irays = (int) num_rays;
    Ray ray = null;
    int two = 0;
    // init array
    float[] val = new float[bins];

    while (len < fileLength) {
      int rayoffset = 0;
      int rayoffset1 = 0;
      int datalen = 0;

      cur_len = len;

      if (nsweep == number_sweeps & rays_count == beg) {
        return;
      }

      if (beg_rec) {

        // --- <raw_prod_bhdr>  12bytes -----------
        raf.seek(cur_len);
        nrec = raf.readShort();    // cur_len
        nsweep = raf.readShort();    // cur_len+2
        byteoff = raf.readShort();
        len = len + 2;            // cur_len+4
        nray = raf.readShort();
        len = len + 2;            // cur_len+6

        // ---- end of <raw_prod_bhdr> -------------
        cur_len = cur_len + 12;
        beg_rec = false;
      }

      if ((nsweep <= number_sweeps) & (rays_count % beg == 0)) {

        // --Read <ingest_data_hdrs> Number of them=nparams*number_sweeps -----
        // ---Len of <ingest_data_hdr>=76 bytes -----------------
        beg = 0;

        for (int i = 0; i < nparams; i++) {
          int idh_len = cur_len + 12 + i * 76;

          raf.seek(idh_len);

          // Read seconds since midnight
          base_time[nu] = raf.readInt();         // idh_len
          raf.skipBytes(2);
          year[nu] = raf.readShort();           // idh_len+6
          month[nu] = raf.readShort();           // idh_len+8
          day[nu] = raf.readShort();           // idh_len+10
          nu++;
          num_sweep[i] = raf.readShort();     // idh_len+12
          num_rays_swp[i] = raf.readShort();     // idh_len+14
          indx_1ray[i] = raf.readShort();     // idh_len+16
          raf.skipBytes(2);
          num_rays_act[i] = raf.readShort();
          beg += num_rays_act[i];    // idh_len+20
          angl_swp[i] = raf.readShort();     // idh_len+22
          bin_len[i] = raf.readShort();     // idh_len+24
          data_type[i] = raf.readShort();     // idh_len+26
        }

        cur_len = cur_len + nparams * 76;
      }

      len = cur_len;

      if (end_rec) {

        // --- Read compression code=2 bytes from cur_len
        raf.seek(cur_len);
        a0 = raf.readShort();
        cur_len = cur_len + 2;

        // --- Check if the code=1 ("1" means an end of a ray)
        if (a0 == (short) 1) {
          if (cur_len % REC_SIZE == 0) {
            beg_rec = true;
            end_rec = true;
            rays_count++;
            read_ray_hdr = true;
            pos = 0;
            data_read = 0;
            nb = 0;
            len = cur_len;
          } else {
            end_rec = true;
            len = cur_len;
            rays_count++;
          }

          continue;
        }

        nwords = a0 & 0x7fff;
        end_words = nwords - 6;
        data_read = end_words * 2;
        end_rec = false;

        if (cur_len % REC_SIZE == 0) {
          len = cur_len;
          read_ray_hdr = true;
          beg_rec = true;

          continue;
        }
      }

      len = cur_len;

      // ---Define output data files for each data_type (= nparams)/sweep ---------
      dty = data_type[0];

      if (nparams > 1) {
        kk = rays_count % nparams;
        col = rays_count / nparams;
        dty = data_type[kk];
      } else if (number_sweeps > 1) {
        kk = nsweep - 1;
        col = rays_count % irays;
      }

      String var_name = data_name[dty];

      // --- read ray_header (size=12 bytes=6 words)---------------------------------------
      if (read_ray_hdr) {
        if (pos_ray_hdr < 2) {
          raf.seek(cur_len);
          beg_az = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;

          if (cur_len % REC_SIZE == 0) {
            pos_ray_hdr = 2;
            beg_rec = true;
            read_ray_hdr = true;

            continue;
          }
        }
        if (pos_ray_hdr < 4) {
          raf.seek(cur_len);
          beg_elev = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;

          if (cur_len % REC_SIZE == 0) {
            pos_ray_hdr = 4;
            beg_rec = true;
            read_ray_hdr = true;
            continue;
          }
        }
        if (pos_ray_hdr < 6) {
          raf.seek(cur_len);
          end_az = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;

          if (cur_len % REC_SIZE == 0) {
            pos_ray_hdr = 6;
            beg_rec = true;
            read_ray_hdr = true;
            continue;
          }
        }
        if (pos_ray_hdr < 8) {
          raf.seek(cur_len);
          end_elev = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;

          if (cur_len % REC_SIZE == 0) {
            pos_ray_hdr = 8;
            beg_rec = true;
            read_ray_hdr = true;

            continue;
          }
        }

        if (pos_ray_hdr < 10) {
          raf.seek(cur_len);
          num_bins = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;

          if (num_bins % 2 != 0) {
            num_bins = (short) (num_bins + 1);
          }
          num_gates[nsweep - 1] = (int) num_bins;
          if (cur_len % REC_SIZE == 0) {
            pos_ray_hdr = 10;
            beg_rec = true;
            read_ray_hdr = true;

            continue;
          }
        }

        if (pos_ray_hdr < 12) {
          raf.seek(cur_len);
          time_start_sw = raf.readShort();
          cur_len = cur_len + 2;
          len = cur_len;
        }
      }

      // ---------- end of ray header ----------------------------------------------
      az = SigmetIOServiceProvider.calcAz(beg_az, end_az);
      elev = SigmetIOServiceProvider.calcElev(end_elev);
      step = SigmetIOServiceProvider.calcStep(range_first, range_last, num_bins);

      if (cur_len % REC_SIZE == 0) {
        len = cur_len;
        beg_rec = true;
        read_ray_hdr = false;

        continue;
      }

      if (pos > 0) {
        data_read = data_read - pos;
        pos = 0;
      }
      //  if(az > 358.5 && az < 358.8 && var_name.contains("Reflectivity")) {
      //      System.out.println(" here ");
      //  }
      if (data_read > 0) {
        raf.seek(cur_len);
        rayoffset = cur_len;
        datalen = data_read;

        for (int i = 0; i < data_read; i++) {
          //  data   = raf.readByte();
          //  dd[nb] = SigmetIOServiceProvider.calcData(recHdr, dty, data);
          cur_len++;
          nb++;

          if (cur_len % REC_SIZE == 0) {
            pos = i + 1;
            beg_rec = true;
            read_ray_hdr = false;
            len = cur_len;
            raf.seek(cur_len);
            break;
          }
        }
        raf.seek(cur_len);
        if (pos > 0) {
          continue;
        }
      }

      if (cur_len % REC_SIZE == 0) {
        pos = 0;
        beg_rec = true;
        read_ray_hdr = false;
        data_read = 0;
        len = cur_len;

        continue;
      }

      raf.seek(cur_len);
      rayoffset1 = cur_len;

      while (nb < (int) num_bins) {
        a00 = raf.readShort();
        cur_len = cur_len + 2;

        // --- Check if the code=1 ("1" means an end of a ray)
        if (a00 == (short) 1) {
          // for (int uk = 0; uk < (int) num_bins; uk++) {
          //  dd[uk] = -999.99f;
          //  }
          ray = new Ray(-999.99f, -999.99f, -999.99f, -999.99f, num_bins, (short) (-99), -999, 0, -999,
                  nsweep, var_name, dty);
          rays_count++;
          beg_rec = false;
          end_rec = true;

          break;
        }

        if (a00 < 0) {    // -- This is data
          nwords = a00 & 0x7fff;
          data_read = nwords * 2;

          if (cur_len % REC_SIZE == 0) {
            pos = 0;
            beg_rec = true;
            end_rec = false;
            len = cur_len;
            read_ray_hdr = false;

            break;
          }

          raf.seek(cur_len);

          for (int ii = 0; ii < data_read; ii++) {
            //   data    = raf.readByte();
            //    dd[nb]  = SigmetIOServiceProvider.calcData(recHdr, dty, data);
            cur_len = cur_len + 1;
            nb = nb + 1;

            if (cur_len % REC_SIZE == 0) {
              pos = ii + 1;
              beg_rec = true;
              end_rec = false;
              len = cur_len;
              read_ray_hdr = false;
              raf.seek(cur_len);
              break;
            }
          }
          raf.seek(cur_len);
          if (pos > 0) {
            break;
          }
        } else if (a00 > 0 & a00 != 1) {
          num_zero = a00 * 2;

          // for (int k = 0; k < num_zero; k++) {
          //   dd[nb + k] = SigmetIOServiceProvider.calcData(recHdr, dty, (byte) 0);
          // }

          nb = nb + num_zero;

          if (cur_len % REC_SIZE == 0) {
            beg_rec = true;
            end_rec = false;
            read_ray_hdr = false;
            pos = 0;
            data_read = 0;
            len = cur_len;

            break;
          }
        }
      }                     // ------ end of while for num_bins---------------------------------

      if (cur_len % REC_SIZE == 0) {
        len = cur_len;

        continue;
      }

      raf.seek(cur_len);

      if (nb == (int) num_bins) {
        a00 = raf.readShort();
        cur_len = cur_len + 2;
        end_rec = true;
        ray = new Ray(range_first, step, az, elev, num_bins, time_start_sw, rayoffset, datalen, rayoffset1,
                nsweep, var_name, dty);
        rays_count++;
        two++;
        if ((nsweep == number_sweeps) & (rays_count % beg == 0)) {
          if (var_name.trim().equalsIgnoreCase("TotalPower")) {
            totalPower.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Reflectivity")) {
            reflectivity.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Velocity")) {
            velocity.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Width")) {
            width.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("DifferentialReflectivity")) {
            diffReflectivity.add(ray);
          } else {
            System.out.println(" Error: Unknown Radial Variable found!!");
          }
          break;
        }

        if (cur_len % REC_SIZE == 0) {
          beg_rec = true;
          end_rec = true;
          read_ray_hdr = true;
          pos = 0;
          data_read = 0;
          nb = 0;
          len = cur_len;
          if (var_name.trim().equalsIgnoreCase("TotalPower")) {
            totalPower.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Reflectivity")) {
            reflectivity.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Velocity")) {
            velocity.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("Width")) {
            width.add(ray);
          } else if (var_name.trim().equalsIgnoreCase("DifferentialReflectivity")) {
            diffReflectivity.add(ray);
          } else {
            System.out.println(" Error: Unknown Radial Variable found!!");
          }
          continue;
        }
      }

      // "TotalPower", "Reflectivity", "Velocity", "Width", "DifferentialReflectivity"
      if (firstRay == null) firstRay = ray;

      if (var_name.trim().equalsIgnoreCase("TotalPower")) {
        totalPower.add(ray);
      } else if (var_name.trim().equalsIgnoreCase("Reflectivity")) {
        reflectivity.add(ray);
      } else if (var_name.trim().equalsIgnoreCase("Velocity")) {
        velocity.add(ray);
      } else if (var_name.trim().equalsIgnoreCase("Width")) {
        width.add(ray);
      } else if (var_name.trim().equalsIgnoreCase("DifferentialReflectivity")) {
        diffReflectivity.add(ray);
      } else {
        System.out.println(" Error: Unknown Radial Variable found!!");
      }

      pos = 0;
      data_read = 0;
      nb = 0;
      read_ray_hdr = true;
      pos_ray_hdr = 0;

      if ((nsweep <= number_sweeps) & (rays_count % beg == 0)) {
        beg_rec = true;
        end_rec = true;
        rays_count = 0;
        nb = 0;
        cur_len = REC_SIZE * (nrec + 1);
        len = cur_len;
        read_ray_hdr = true;
      }

      len = cur_len;
    }    // ------------end of outer while  ---------------
    lastRay = ray;

    if (reflectivity.size() > 0) {
      reflectivityGroups = sortScans("reflectivity", reflectivity, 1000);
      hasReflectivity = true;
    }
    if (velocity.size() > 0) {
      velocityGroups = sortScans("velocity", velocity, 1000);
      hasVelocity = true;
    }
    if (totalPower.size() > 0) {
      totalPowerGroups = sortScans("totalPower", totalPower, 1000);
      hasTotalPower = true;
    }
    if (width.size() > 0) {
      widthGroups = sortScans("width", width, 1000);
      hasWidth = true;
    }
    if (diffReflectivity.size() > 0) {
      differentialReflectivityGroups = sortScans("diffReflectivity", diffReflectivity, 1000);
      hasDifferentialReflectivity = true;
    }

    if (time.size() > 0) {
      timeGroups = sortScans("diffReflectivity", diffReflectivity, 1000);
      hasTime = true;
    }

    // --------- fill all of values in the ncfile ------
  }    // ----------- end of doData -----------------------

  private int max_radials = 0;
  private int min_radials = Integer.MAX_VALUE;
  private boolean debugRadials = false;

  private List<List<Ray>> sortScans(String name, List<Ray> scans, int siz) {

    // now group by elevation_num
    Map<Short, List<Ray>> groupHash = new HashMap<Short, List<Ray>>(siz);

    for (Ray ray : scans) {
      List<Ray> group = groupHash.get((short) ray.nsweep);

      if (null == group) {
        group = new ArrayList<Ray>();
        groupHash.put((short) ray.nsweep, group);
      }

      group.add(ray);
    }

    for (Short aShort : groupHash.keySet()) {
      List<Ray> group = groupHash.get(aShort);
      Ray[] rr = new Ray[group.size()];
      group.toArray(rr);
      checkSort(rr);
    }

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

    // use the maximum radials
    for (List<Ray> group : groups) {
      max_radials = Math.max(max_radials, group.size());
      min_radials = Math.min(min_radials, group.size());
    }

    if (debugRadials) {
      System.out.println(name + " min_radials= " + min_radials + " max_radials= " + max_radials);

      for (List<Ray> group : groups) {
        Ray lastr = group.get(0);

        for (int j = 1; j < group.size(); j++) {
          Ray r = group.get(j);
          if (r.getTime() < lastr.getTime()) {
            System.out.println(" out of order " + j);
          }
          lastr = r;
        }
      }
    }

    return groups;
  }

  private class GroupComparator implements Comparator<List<Ray>> {
    public int compare(List<Ray> group1, List<Ray> group2) {
      Ray record1 = group1.get(0);
      Ray record2 = group2.get(0);

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

      // return record1.cut - record2.cut;
    }
  }

  public List<List<Ray>> getTotalPowerGroups() {
    return totalPowerGroups;
  }

  public List<List<Ray>> getVelocityGroups() {
    return velocityGroups;
  }

  public List<List<Ray>> getWidthGroups() {
    return widthGroups;
  }

  public List<List<Ray>> getReflectivityGroups() {
    return reflectivityGroups;
  }

  public List<List<Ray>> getDifferentialReflectivityGroups() {
    return differentialReflectivityGroups;
  }

  public int[] getNumberGates() {
    return num_gates;
  }

  public int[] getStartSweep() {
    return base_time;
  }

  /**
   * Sort Ray objects in the same sweep according to the ascended azimuth (from 0 to 360)
   * and time.
   *
   * @param r the array of Ray objects in a sweep. Its length=number_rays
   */
  void checkSort(Ray[] r) {
    int j = 0, n = 0, n1 = 0, n2 = 0;
    short time1 = 0, time2 = 0;
    int[] k1 = new int[300];
    int[] k2 = new int[300];
    //      define the groups of rays with the same "time". For ex.:
    //      group1 - ray[0]={time=1,az=344}, ray[1]={time=1,az=345}, ... ray[11]={time=1,az=359}
    //      group2 - ray[12]={time=1,az=0}, ray[13]={time=1,az=1}, ... ray[15]={time=1,az=5}
    //      k1- array of begin indx (0,12), k2- array of end indx (11,15)
    for (int i = 0; i < r.length - 1; i++) {
      time1 = r[i].getTime();
      time2 = r[i + 1].getTime();
      if (time1 != time2) {
        k2[j] = i;
        j = j + 1;
        k1[j] = i + 1;
      }
    }
    if (k2[j] < r.length - 1) {
      k1[j] = k2[j - 1] + 1;
      k2[j] = r.length - 1;
      n = j + 1;
    }

    //      if different groups have the same value of "time" (may be 2 and more groups) -
    //      it1= indx of "k1" of 1st group, it2= indx of "k2" of last group
    int it1 = 0, it2 = 0;
    for (int ii = 0; ii < j + 1; ii++) {
      n1 = k1[ii];
      for (int i = 0; i < j + 1; i++) {
        if (i != ii) {
          n2 = k1[i];
          if (r[n1].getTime() == r[n2].getTime()) {
            it1 = ii;
            it2 = i;
          }
        }
      }
    }

    n1 = k1[it1];
    n2 = k1[it2];
    int s1 = k2[it1] - k1[it1] + 1;
    int s2 = k2[it2] - k1[it2] + 1;
    float[] t0 = new float[s1];
    float[] t00 = new float[s2];
    for (int i = 0; i < s1; i++) {
      t0[i] = r[n1 + i].getAz();
    }
    for (int i = 0; i < s2; i++) {
      t00[i] = r[n2 + i].getAz();
    }
    float mx0 = t0[0];
    for (int i = 0; i < s1; i++) {
      if (mx0 < t0[i]) mx0 = t0[i];
    }
    float mx00 = t00[0];
    for (int i = 0; i < s2; i++) {
      if (mx00 < t00[i]) mx00 = t00[i];
    }
    if ((mx0 > 330.0f & mx00 < 50.0f)) {
      for (int i = 0; i < s1; i++) {
        float q = r[n1 + i].getAz();
        r[n1 + i].setAz(q - 360.0f);
      }
    }
    Arrays.sort(r, new RayComparator());
    for (int i = 0; i < r.length; i++) {
      float a = r[i].getAz();
      if (a < 0 & a > -361.0f) {
        float qa = r[i].getAz();
        r[i].setAz(qa + 360.0f);
      }
    }

  }

  class RayComparator implements Comparator<Ray> {
    public int compare(Ray ray1, Ray ray2) {
      if (ray1.getTime() < ray2.getTime()) {
        return -1;
      } else if (ray1.getTime() == ray2.getTime()) {
        if (ray1.getAz() < ray2.getAz()) {
          return -1;
        }
        if (ray1.getAz() > ray2.getAz()) {
          return 1;
        }
        if (ray1.getAz() == ray2.getAz()) {
          return 0;
        }
      } else if (ray1.getTime() > ray2.getTime()) {
        return 1;
      }
      return 0;
    }
  } // class RayComparator end ----------------------------------

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.util;

import java.lang.reflect.*;

/**
 * @author Nathan David Potter
 */

public abstract class Tools {

    //#*******************************************************************************

    /**
     * Show me lots of stuff about the passed in object
     */
    public static void probeObject(Object o) {

        Class c = o.getClass();

        Class interfaces[] = c.getInterfaces();
        Class parent = c.getSuperclass();
        Method m[] = c.getMethods();

        System.out.println("********* OBJECT PROBE *********");
        System.out.println("Class Name:  " + c.getName());
        System.out.println("Super Class: " + parent.getName());
        System.out.println("Interfaces: ");
        for (int i = 0; i < interfaces.length; i++) {
            System.out.println("    " + interfaces[i].getName());
        }

        System.out.println("Methods:");
        for (int i = 0; i < m.length; i++) {

            Class params[] = m[i].getParameterTypes();
            Class excepts[] = m[i].getExceptionTypes();
            Class ret = m[i].getReturnType();

            System.out.print("    " + ret.getName() + "  " + m[i].getName() + "(");

            for (int j = 0; j < params.length; j++) {
                if (j > 0)
                    System.out.print(", ");
                System.out.print(params[j].getName());
            }
            System.out.print(")  throws ");
            for (int j = 0; j < excepts.length; j++) {
                if (j > 0)
                    System.out.print(", ");
                System.out.print(excepts[j].getName());
            }
            System.out.println("");
        }
        System.out.println("******************");

    }
    //#*******************************************************************************

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.dap;

import java.io.*;

/**
 * A vector of shorts. (as in 16bit ints :)
 *
 * @author npotter
 * @version $Revision: 15901 $
 * @see PrimitiveVector
 */
public class Int16PrimitiveVector extends PrimitiveVector
{
    /**
     * the array of short values.
     */
    private short vals[];

    /**
     * Constructs a new Int16PrimitiveVector.
     *
     * @param var the template BaseType to use.
     */
    public Int16PrimitiveVector(BaseType var) {
        super(var);
    }

    /**
     * Returns the number of elements in the array.
     *
     * @return the number of elements in the array.
     */
    public int getLength() {
        return vals.length;
    }

    /**
     * Sets the number of elements in the array.  Allocates a new primitive
     * array of the desired size.  Note that if this is called multiple times,
     * the old array and its contents will be lost.
     *

     * Only called inside of deserialize method or in derived
     * classes on server.
     *
     * @param len the number of elements in the array.
     */
    public void setLength(int len) {
        vals = new short[len];
    }

    /**
     * Return the i'th value as a short.
     *
     * @param i the index of the value to return.
     * @return the i'th value.
     */
    public final short getValue(int i) {
        return vals[i];
    }

    /**
     * Set the i'th value of the array.
     *
     * @param i      the index of the value to set.
     * @param newVal the new value.
     */
    public final void setValue(int i, short newVal) {
        vals[i] = newVal;
    }

    /**
     * Prints the value of all variables in this vector.  This
     * method is primarily intended for debugging OPeNDAP applications and
     * text-based clients such as geturl.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param space this value is passed to the printDecl method,
     *              and controls the leading spaces of the output.
     * @see BaseType#printVal(PrintWriter, String, boolean)
     */
    public  void printVal(PrintWriter os, String space) {
        int len = vals.length;
        for (int i = 0; i < len - 1; i++) {
            os.print(vals[i]);
            os.print(", ");
        }
        // print last value, if any, without trailing comma
        if (len > 0)
            os.print(vals[len - 1]);
    }

    /**
     * Prints the value of a single variable in this vector.
     * method is used by DArray's printVal method.
     *
     * @param os    the PrintWriter on which to print the value.
     * @param index the index of the variable to print.
     * @see DArray#printVal(PrintWriter, String, boolean)
     */
    public void printSingleVal(PrintWriter os, int index) {
        os.print(vals[index]);
    }

    /**
     * Reads data from a DataInputStream. This method is only used
     * on the client side of the OPeNDAP client/server connection.
     *
     * @param source   a DataInputStream to read from.
     * @param sv       The ServerVersion returned by the server.
     *                 (used by DSequence to determine which protocol version was
     *                 used).
     * @param statusUI The StatusUI object to use for GUI updates
     *                 and user cancellation notification (may be null).
     * @throws DataReadException when invalid data is read, or if the user
     *                           cancels the download.
     * @throws EOFException      if EOF is found before the variable is completely
     *                           deserialized.
     * @throws IOException       thrown on any other InputStream exception.
     * @see ClientIO#deserialize(DataInputStream, ServerVersion, StatusUI)
     */
    public synchronized void deserialize(DataInputStream source,
                                         ServerVersion sv,
                                         StatusUI statusUI)
            throws IOException, EOFException, DataReadException {
        for (int i = 0; i < vals.length; i++) {
            vals[i] = (short) source.readInt();
            if (statusUI != null) {
                statusUI.incrementByteCount(4);
                if (statusUI.userCancelled())
                    throw new DataReadException("User cancelled");
            }
        }
    }

    /**
     * Writes data to a DataOutputStream. This method is used
     * primarily by GUI clients which need to download OPeNDAP data, manipulate
     * it, and then re-save it as a binary file.
     *
     * @param sink a DataOutputStream to write to.
     * @throws IOException thrown on any OutputStream
     *                     exception.
     */
    public void externalize(DataOutputStream sink) throws IOException {
        for (int i = 0; i < vals.length; i++) {
            sink.writeInt((int) (vals[i]));
        }
    }

    /**
     * Write a subset of the data to a DataOutputStream.
     *
     * @param sink    a DataOutputStream to write to.
     * @param start  starting index (i=start)
     * @param stop   ending index (i<=stop)
     * @param stride index stride (i+=stride)
     * @throws IOException thrown on any OutputStream exception.
     */
    public void externalize(DataOutputStream sink, int start, int stop, int stride) throws IOException {
        for (int i = start; i <= stop; i += stride)
            sink.writeInt((int) vals[i]);
    }

    /**
     * Returns (a reference to) the internal storage for this PrimitiveVector
     * object.
     *
WARNING:

     * Because this method breaks encapsulation rules the user must beware!
     * If we (the OPeNDAP prgramming team) choose to change the internal
     * representation(s) of these types your code will probably break.
     *

     * This method is provided as an optimization to eliminate massive
     * copying of data.
     *
     * @return The internal array of shorts.
     */
    public Object getInternalStorage() {
        return (vals);
    }

    /**
     * Set the internal storage for PrimitiveVector.
     *
WARNING:

     * Because this method breaks encapsulation rules the user must beware!
     * If we (the OPeNDAP prgramming team) choose to change the internal
     * representation(s) of these types your code will probably break.
     *

     * This method is provided as an optimization to eliminate massive
     * copying of data.
     */
    public void setInternalStorage(Object o) {
        vals = (short []) o;
    }

    /**
     * Create a new primitive vector using a subset of the data.
     *
     * @param start  starting index (i=start)
     * @param stop   ending index (i<=stop)
     * @param stride index stride (i+=stride)
     * @return new primitive vector, of type Int16PrimitiveVector.
     */
    public PrimitiveVector subset(int start, int stop, int stride) {
        Int16PrimitiveVector n = new Int16PrimitiveVector(getTemplate());
        stride = Math.max(stride, 1);
        stop = Math.max(start, stop);
        int length = 1 + (stop - start) / stride;
        n.setLength(length);

        int count = 0;
        for (int i = start; i <= stop; i += stride) {
            n.setValue(count, vals[i]);
            count++;
        }
        return n;
    }

/**
   * Prints the value of the variable, with its declaration.  This
   * function is primarily intended for debugging OPeNDAP applications and
   * text-based clients such as geturl.
   *
   * @param os           the PrintWriter on which to print the value.
   * @param space        this value is passed to the printDecl method,
   *                     and controls the leading spaces of the output.
   * @param print_decl_p a boolean value controlling whether the
   *                     variable declaration is printed as well as the value.
   */
   public void printVal(PrintWriter os, String space,
                                boolean print_decl_p) {}

    /**
     * Returns a clone of this Int16PrimitiveVector.
     * See DAPNode.cloneDag()
     *
     * @param map track previously cloned nodes
     * @return a clone of this Int16PrimitiveVector.
     */
    public DAPNode cloneDAG(CloneMap map)
        throws CloneNotSupportedException
    {
        Int16PrimitiveVector v = (Int16PrimitiveVector) super.cloneDAG(map);
        if (vals != null) {
            v.vals = new short[vals.length];
            System.arraycopy(vals, 0, v.vals, 0, vals.length);
        }
        return v;
    }

}


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

import ucar.nc2.util.CancelTask;
import ucar.nc2.*;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.dataset.DatasetConstructor;
import ucar.ma2.*;

import java.io.IOException;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;
import java.util.EnumSet;

/**
 * Tiled Aggregation.
 *
 * @author caron
 * @since Aug 16, 2007
 */
public class AggregationTiled extends Aggregation implements ProxyReader {
  private List<String> dimNames = new ArrayList<String>();
  private List<Dimension> dims = new ArrayList<Dimension>();
  private Section section;

  private boolean debug = false;

  public AggregationTiled(NetcdfDataset ncd, String dimName, String recheckS) {
    super(ncd, dimName, Aggregation.Type.tiled, recheckS);

    // parse the tiling dimension names
    StringTokenizer stoke = new StringTokenizer(dimName);
    while (stoke.hasMoreTokens()) {
      dimNames.add(stoke.nextToken());
    }
  }

  @Override
  protected void buildNetcdfDataset(CancelTask cancelTask) throws IOException {
    // open a "typical"  nested dataset and copy it to newds
    Dataset typicalDataset = getTypicalDataset();
    NetcdfFile typical = typicalDataset.acquireFile(null);
    DatasetConstructor.transferDataset(typical, ncDataset, null);

    // find the tiling dimensions
    for (String dimName : dimNames) {
      Dimension dim = ncDataset.getRootGroup().findDimension(dimName); // dimension is from new dataset
      if (null != dim)
        dims.add(dim);
      else
        throw new IllegalArgumentException("Unknown dimension = " + dimName);
    }

    // run through the datasets to get the union of the ranges
    // add names to the dataset sections while were there
    Section result = null;
    for (Dataset d : datasets) {
      DatasetTiled dt = (DatasetTiled) d;
      try {
        dt.section = dt.section.addRangeNames(dimNames);
        result = (result == null) ? dt.section : result.union(dt.section);
      } catch (InvalidRangeException e) {
        throw new IllegalArgumentException(e);
      }
    }

    // sanity checks
    assert result != null;
    assert result.getRank() == dims.size();
    for (Range r : result.getRanges()) {
      assert r.first() == 0;
      assert r.stride() == 1;
    }
    section = result;

    // set dimension lengths to union length
    int count = 0;
    for (Range r : section.getRanges()) {
      Dimension dim = dims.get(count++);
      dim.setLength(r.length());
    }

    // run through all variables
    for (Variable v : typical.getVariables()) {
      if (isTiled(v)) {
        Group newGroup = DatasetConstructor.findGroup(ncDataset, v.getParentGroup());
        VariableDS vagg = new VariableDS(ncDataset, newGroup, null, v.getShortName(), v.getDataType(),
                v.getDimensionsString(), null, null);   // LOOK what about anon dimensions?
        vagg.setProxyReader( this); // do the reading here
        DatasetConstructor.transferVariableAttributes(v, vagg);

        newGroup.removeVariable(v.getShortName());
        newGroup.addVariable(vagg);
        // aggVars.add(vagg);
      }
      if (cancelTask != null && cancelTask.isCancel()) return;
    }

    setDatasetAcquireProxy(typicalDataset, ncDataset);
    typicalDataset.close(typical); // close it because we use DatasetProxyReader to acquire

    ncDataset.finish();
  }

  // a variable is tiled if any of its dimensions are tiled
  private boolean isTiled(Variable v) {
    for (Dimension d : v.getDimensions()) {
      for (Range r : section.getRanges()) {
        if (d.getShortName().equals(r.getName()))
          return true;
      }
    }
    return false;
  }

  @Override
  protected void rebuildDataset() throws IOException {
    ncDataset.empty();
    dims = new ArrayList<Dimension>();
    buildNetcdfDataset(null);
  }

  @Override
  public Array reallyRead(Variable mainv, CancelTask cancelTask) throws IOException {

    DataType dtype = (mainv instanceof VariableDS) ? ((VariableDS) mainv).getOriginalDataType() : mainv.getDataType();
    Array allData = Array.factory(dtype, mainv.getShape()); // LOOK need fill
    Section wantSection = mainv.getShapeAsSection();
    if (debug) System.out.println("wantSection: " + wantSection + " for var " + mainv.getFullName());

    // make concurrent
    List<Dataset> nestedDatasets = getDatasets();
    for (Dataset vnested : nestedDatasets) {
      DatasetTiled dtiled = (DatasetTiled) vnested;

      // construct the "dataSection" by replacing the tiled dimensions
      Section tiledSection = dtiled.makeVarSection(mainv);
      //System.out.println(" tiledSection: " + tiledSection);

      // now use a TileLayout to figure out how to "distribute" it to the result array
      Array varData;
      TileLayout index;
      try {
        // read in the entire data from this nested dataset
        varData = dtiled.read(mainv, cancelTask);
        if (varData == null)
          throw new IOException("cant read "+mainv.getFullName());

        index = new TileLayout(tiledSection, wantSection);

        if (debug) System.out.println(" varData read: " + new Section(varData.getShape()));
      } catch (InvalidRangeException e) {
        throw new IllegalArgumentException(e.getMessage());
      }

      while (index.hasNext()) {
        try {
          Array.arraycopy(varData, index.srcPos, allData, index.resultPos, index.nelems);
        } catch (RuntimeException e) {
          System.out.println(index.toString());
          throw e;
        }
      }

      // covers the case of coordinate variables for a 1 row or 1 col tiling.
      // doesnt eliminate duplicate reading in general
      if (varData.getSize() == mainv.getSize()) break;

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

    return allData;
  }

  @Override
  public Array reallyRead(Variable mainv, Section wantSection, CancelTask cancelTask) throws IOException {

    // If its full sized, then use full read, so that data might get cached.
    long size = wantSection.computeSize();
    if (size == mainv.getSize())
      return reallyRead(mainv, cancelTask);

    DataType dtype = (mainv instanceof VariableDS) ? ((VariableDS) mainv).getOriginalDataType() : mainv.getDataType();
    Array allData = Array.factory(dtype, wantSection.getShape()); // LOOK need fill
    if (debug) {
      System.out.println(dtype + " allData allocated: " + new Section(allData.getShape()));
    }

    // make concurrent

    // run through all the datasets
    List<Dataset> nestedDatasets = getDatasets();
    for (Dataset vnested : nestedDatasets) {
      DatasetTiled dtiled = (DatasetTiled) vnested;
      Section tiledSection = dtiled.makeVarSection(mainv);
      TileLayout index;
      Array varData;

      try {
        if (!tiledSection.intersects(wantSection))
          continue;

        // read in the desired section of data from this nested dataset
        Section needToRead = tiledSection.intersect(wantSection); // the part we need to read

        if (debug) System.out.println(" tiledSection: " + tiledSection + " from file " + dtiled.getLocation());
        if (debug) System.out.println(" intersection: " + needToRead);

        Section localNeed = needToRead.shiftOrigin(tiledSection); // shifted to the tiled section
        varData = dtiled.read(mainv, cancelTask, localNeed.getRanges());
        if (varData == null)
          throw new IOException("cant read "+mainv.getFullName());

        index = new TileLayout(needToRead, wantSection);

      } catch (InvalidRangeException e) {
        throw new IllegalArgumentException(e.getMessage());
      }

      while (index.hasNext()) {
        try {
          Array.arraycopy(varData, index.srcPos, allData, index.resultPos, index.nelems);
        } catch (RuntimeException e) {
          System.out.println(" tiledSection: " + tiledSection);
          System.out.println(index.toString());
          throw e;
        }
      }

      // covers the case of coordinate variables for a 1 row or 1 col tiling.
      // doesnt eliminate duplicate reading in general
      if (varData.getSize() == mainv.getSize()) break;

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

    return allData;
  }

  private class TileLayout {
    //Section dataSection, resultSection;
    private int srcPos = 0, resultPos, nelems;
    private int total, startElem;
    Index index;

    TileLayout(Section localSection, Section wantSection) throws InvalidRangeException {
      Section dataSection = localSection.compact();
      Section resultSection = wantSection.compact();
      if (debug) System.out.println(" resultSection: " + resultSection);
      if (debug) System.out.println(" dataSection: " + dataSection);

      int rank = dataSection.getRank();

      // total elements to transfer
      total = (int) dataSection.computeSize();

      // figure out the offset
      long product = 1;
      startElem = 0; // offset in want
      for (int ii = rank - 1; ii >= 0; ii--) {
        int d = dataSection.getOrigin(ii) - resultSection.getOrigin(ii);
        if (d > 0) startElem += product * d;
        product *= resultSection.getShape(ii);
      }
      resultPos = startElem;

      // we will use an Index object to keep track of the chunks
      // last range length is nelems; reduce index rank
      nelems = localSection.getShape(rank - 1);
      int[] stride = new int[rank - 1];
      int[] shape = new int[rank - 1];

      product = resultSection.getShape(rank - 1);
      for (int ii = rank - 2; ii >= 0; ii--) {
        stride[ii] = (int) product;
        shape[ii] = dataSection.getShape(ii);
        product *= resultSection.getShape(ii);
      }
      index = new Index(shape, stride);
    }

    boolean first = true;

    boolean hasNext() {
      if (first) {
        first = false;
        return true;
      }

      srcPos += nelems;
      if (srcPos >= total)
        return false;

      index.incr();
      resultPos = startElem + index.currentElement();
      return true;
    }

    public String toString() {
      return "  nElems: " + nelems + " srcPos: " + srcPos + " resultPos: " + resultPos;
    }
  }

  @Override
  protected Dataset makeDataset(String cacheName, String location, String id, String ncoordS, String coordValueS, String sectionSpec,
                                EnumSet<NetcdfDataset.Enhance> enhance, ucar.nc2.util.cache.FileFactory reader) {
    return new DatasetTiled(cacheName, location, id, sectionSpec, enhance, reader);
  }

  /**
   * Encapsolates a NetcdfFile that is a component of the aggregation.
   */
  class DatasetTiled extends Dataset {
    protected String sectionSpec;
    protected Section section;

    /**
     * Dataset constructor.
     * With this constructor, the actual opening of the dataset is deferred, and done by the reader.
     * Used with explicit netcdf elements, and scanned files.
     *
     * @param cacheName   a unique name to use for caching
     * @param location    attribute "location" on the netcdf element
     * @param id          attribute "id" on the netcdf element
     * @param sectionSpec attribute "sectionSpec" on the netcdf element
     * @param enhance     open dataset in enhance mode
     * @param reader      factory for reading this netcdf dataset; if null, use NetcdfDataset.open( location)
     */
    protected DatasetTiled(String cacheName, String location, String id, String sectionSpec, EnumSet<NetcdfDataset.Enhance> enhance,
                           ucar.nc2.util.cache.FileFactory reader) {
      super(cacheName, location, id, enhance, reader);
      this.sectionSpec = sectionSpec;

      try {
        section = new Section(sectionSpec);
      } catch (InvalidRangeException e) {
        throw new IllegalArgumentException(e);
      }
    }

    boolean isNeeded(Section wantSection) throws InvalidRangeException {
      return section.intersects(wantSection);
    }

    // construct the Variable section pertaining to this Datatset by replacing the tiled dimensions
    Section makeVarSection(Variable mainv) {
      Section vSection = mainv.getShapeAsSection();
      Section dataSection = new Section();
      for (Range r : vSection.getRanges()) {
        Range rr = section.find(r.getName());
        dataSection.appendRange(rr != null ? rr : r);
      }
      return dataSection;
    }
  }
}


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

package ucar.nc2.grib.coord;

import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarPeriod;

import javax.annotation.concurrent.Immutable;

/**
 * Abstract superclass for time coordinates ( time, timeIntv, time2D)
 * Effectively Immutable
 *
 * @author caron
 * @since 1/23/14
 */
@Immutable
public abstract class CoordinateTimeAbstract implements Coordinate {
  public static final String MIXED_INTERVALS = "Mixed_intervals";
  public static CalendarDateFactory cdf;

  final String periodName;                   // used to create the udunit
  protected final int code;                  // unit of time (Grib1 table 4, Grib2 table 4.4), eg hour, day, month
  protected final CalendarPeriod timeUnit;   // time duration, based on code
  protected final CalendarDate refDate;      // used to create the udunit
  protected final int[] time2runtime;        // for each time, which runtime is used; index into master runtime

  protected String name = "time";

  CoordinateTimeAbstract(int code, CalendarPeriod timeUnit, CalendarDate refDate, int[] time2runtime) {
    this.code = code;
    this.timeUnit = timeUnit;
    this.refDate = (cdf == null) ? refDate : cdf.get(refDate);
    this.time2runtime = time2runtime;

    CalendarPeriod.Field cf = timeUnit.getField();
    if (cf == CalendarPeriod.Field.Month || cf == CalendarPeriod.Field.Year)
      this.periodName = "calendar "+ cf.toString();
    else
      this.periodName = cf.toString();
  }

  @Override
  public int getCode() {
    return code;
  }

  @Override
  public String getUnit() {
    return periodName;
  }

  public String getTimeUdUnit() {
    return periodName + " since " + refDate;
  }

  @Override
  public String getName() {
    return name;
  }

  public void setName(String name) {
    if (!this.name.equals("time")) throw new IllegalStateException("Cant modify");
    this.name = name;
  }

  public CalendarDate getRefDate() {
    return refDate;
  }

  public CalendarPeriod getTimeUnit() {
    return timeUnit;
  }

  public int[] getTime2runtime() {
    return time2runtime;
  }

  public int getMasterRuntimeIndex(int timeIdx) {
    if (time2runtime == null) return -1;
    if (timeIdx < 0 || timeIdx >= time2runtime.length) return -1;
    return time2runtime[timeIdx];
  }

  @Override
  public int getNCoords() {
    return getSize();
  }

  public double getOffsetInTimeUnits(CalendarDate start) {
    return timeUnit.getOffset(start, getRefDate());
  }
  ////////////////////////////////////////

  /**
   * Implements coverting a "complete best" to a "monotonic best".
   * The reftime is not allowed to decrease
   * @return "monotonic best" CoordinateTimeAbstract, based on this one, which is a "complete best"
   */
  public CoordinateTimeAbstract makeBestFromComplete() {
    int[] best = new int[time2runtime.length];
    int last = -1;
    int count = 0;
    for (int i=0; i<time2runtime.length; i++) {
      int time = time2runtime[i];
      if (time >= last) {
        last = time;
        best[i] = time;
        count++;
      } else {
        best[i] = -1;
      }
    }
    return makeBestFromComplete(best, count);
  }

  protected abstract CoordinateTimeAbstract makeBestFromComplete(int[] best, int n);

  public abstract CalendarDateRange makeCalendarDateRange(ucar.nc2.time.Calendar cal);

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.unidata.geoloc.projection;

import ucar.nc2.constants.CF;
import ucar.unidata.geoloc.*;

import ucar.unidata.util.Format;

/**
 * This is the "fake" identity projection where world coord = latlon coord.
 * Topologically its the same as a cylinder tangent to the earth at the equator.
 * The cylinder is cut at the "seam" = centerLon +- 180.
 * Longitude values are always kept in the range [centerLon +-180]
 *
 * @author John Caron
 * @see ProjectionImpl
 */

public class LatLonProjection extends ProjectionImpl {

  /**
   * center longitude
   */
  private double centerLon = 0.0;
  private Earth earth = EarthEllipsoid.DEFAULT;

  @Override
  public ProjectionImpl constructCopy() {
    LatLonProjection result = new LatLonProjection(getName(), getDefaultMapArea());
    result.setDefaultMapArea(defaultMapArea);
    result.setName(name);
    result.earth = this.earth;
    return result;
  }

  /**
   * Default constructor
   */
  public LatLonProjection() {
    this("LatLonProjection");
    addParameters();
  }

  public LatLonProjection(Earth earth) {
    this("LatLonProjection");
    this.earth = earth;
    addParameters();
  }

  /**
   * Create a new LatLonProjection
   *
   * @param name name of projection
   */
  public LatLonProjection(String name) {
    this(name, new ProjectionRect(-180, -90, 180, 90));
    addParameters();
  }

  /**
   * Create a new LatLonProjection
   *
   * @param name           name of projection
   * @param defaultMapArea bounding box
   */
  public LatLonProjection(String name, ProjectionRect defaultMapArea) {
    super(name, true);
    this.defaultMapArea = defaultMapArea;
    addParameters();
  }

  private void addParameters() {
    addParameter(CF.GRID_MAPPING_NAME, CF.LATITUDE_LONGITUDE);
    if (earth.isSpherical())
      addParameter(CF.EARTH_RADIUS, earth.getEquatorRadius());
    else {
      addParameter(CF.SEMI_MAJOR_AXIS, earth.getEquatorRadius());
      addParameter(CF.SEMI_MINOR_AXIS, earth.getPoleRadius());
    }
  }

  /**
   * Get the label to be used in the gui for this type of projection
   *
   * @return Type label
   */
  public String getProjectionTypeLabel() {
    return "Lat/Lon";
  }

  /**
   * Get a String of the parameters
   *
   * @return a String of the parameters
   */
  public String paramsToString() {
    return "Center lon:" + Format.d(centerLon, 3);
  }

  /**
   * See if this projection equals the object in question
   *
   * @param o object in question
   * @return true if it is a LatLonProjection and covers the same area
   */

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    LatLonProjection that = (LatLonProjection) o;
    if ((defaultMapArea == null) != (that.defaultMapArea == null)) return false; // common case is that these are null
    if (defaultMapArea != null && !that.defaultMapArea.equals(defaultMapArea)) return false;

    return Double.compare(that.centerLon, centerLon) == 0;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = Double.doubleToLongBits(centerLon);
    result = (int) (temp ^ (temp >>> 32));
    if (defaultMapArea != null)
      result = 31 * result + defaultMapArea.hashCode();
    return result;
  }

  /**
   * Convert a LatLonPoint to projection coordinates
   *
   * @param latlon convert from these lat, lon coordinates
   * @param result the object to write to
   * @return the given result
   */
  public ProjectionPoint latLonToProj(LatLonPoint latlon, ProjectionPointImpl result) {
    result.setLocation(LatLonPointImpl.lonNormal(latlon.getLongitude(),
        centerLon), latlon.getLatitude());
    return result;
  }

  /**
   * Convert projection coordinates to a LatLonPoint
   * Note: a new object is not created on each call for the return value.
   *
   * @param world  convert from these projection coordinates
   * @param result the object to write to
   * @return LatLonPoint convert to these lat/lon coordinates
   */
  public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
    result.setLongitude(world.getX());
    result.setLatitude(world.getY());
    return result;
  }

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) coordinate of
   *             the ith point
   * @return the "to" array
   */
  public float[][] projToLatLon(float[][] from, float[][] to) {

    float[] fromX = from[INDEX_X];
    float[] fromY = from[INDEX_Y];
    to[INDEX_LAT] = fromY;
    to[INDEX_LON] = fromX;
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates: to[2][n]
   *                 where (to[0][i], to[1][i]) is the (x,y) coordinate of
   *                 the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return the "to" array
   */
  public float[][] latLonToProj(float[][] from, float[][] to, int latIndex,
                                int lonIndex) {
    int cnt = from[0].length;
    float[] toX = to[INDEX_X];
    float[] toY = to[INDEX_Y];
    float[] fromLat = from[latIndex];
    float[] fromLon = from[lonIndex];
    float lat, lon;
    for (int i = 0; i < cnt; i++) {
      lat = fromLat[i];
      lon = (float) (centerLon + Math.IEEEremainder(fromLon[i] - centerLon, 360.0));
      toX[i] = lon;
      toY[i] = lat;
    }
    return to;
  }

  /**
   * Convert projection coordinates to lat/lon coordinate.
   *
   * @param from array of projection coordinates: from[2][n], where
   *             (from[0][i], from[1][i]) is the (x, y) coordinate
   *             of the ith point
   * @param to   resulting array of lat/lon coordinates: to[2][n] where
   *             (to[0][i], to[1][i]) is the (lat, lon) coordinate of
   *             the ith point
   * @return the "to" array
   */
  public double[][] projToLatLon(double[][] from, double[][] to) {

    double[] fromX = from[INDEX_X];
    double[] fromY = from[INDEX_Y];
    to[INDEX_LAT] = fromY;
    to[INDEX_LON] = fromX;
    return to;
  }

  /**
   * Convert lat/lon coordinates to projection coordinates.
   *
   * @param from     array of lat/lon coordinates: from[2][n], where
   *                 (from[latIndex][i], from[lonIndex][i]) is the (lat,lon)
   *                 coordinate of the ith point
   * @param to       resulting array of projection coordinates: to[2][n]
   *                 where (to[0][i], to[1][i]) is the (x,y) coordinate of
   *                 the ith point
   * @param latIndex index of lat coordinate; must be 0 or 1
   * @param lonIndex index of lon coordinate; must be 0 or 1
   * @return the "to" array
   */
  public double[][] latLonToProj(double[][] from, double[][] to,
                                 int latIndex, int lonIndex) {
    int cnt = from[0].length;
    double[] toX = to[INDEX_X];
    double[] toY = to[INDEX_Y];
    double[] fromLat = from[latIndex];
    double[] fromLon = from[lonIndex];
    double lat, lon;
    for (int i = 0; i < cnt; i++) {
      lat = fromLat[i];
      lon = centerLon + Math.IEEEremainder(fromLon[i] - centerLon, 360.0);
      toX[i] = lon;
      toY[i] = lat;
    }
    return to;
  }

  /**
   * Set the center of the Longitude range. It is normalized to  +/- 180.
   * The cylinder is cut at the "seam" = centerLon +- 180.
   * Use this to keep the Longitude values kept in the range [centerLon +-180], which
   * makes seam handling easier.
   *
   * @param centerLon the center of the Longitude range.
   * @return centerLon normalized to +/- 180.
   */
  public double setCenterLon(double centerLon) {
    this.centerLon = LatLonPointImpl.lonNormal(centerLon);
    return this.centerLon;
  }

  /**
   * Get the center of the Longitude range. It is normalized to  +/- 180.
   *
   * @return the center longitude
   */
  public double getCenterLon() {
    return centerLon;
  }

  /**
   * Does the line between these two points cross the projection "seam".
   *
   * @param pt1 the line goes between these two points
   * @param pt2 the line goes between these two points
   * @return false if there is no seam
   */
  public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
    return Math.abs(pt1.getX() - pt2.getX()) > 270.0;  // ?? LOOK: do I believe this
  }

  /**
   * Set a reasonable bounding box for this projection.
   *
   * @param bb a reasonable bounding box
   */
  public void setDefaultMapArea(ProjectionRect bb) {
    super.setDefaultMapArea(bb);
    this.centerLon = bb.getCenterX();
    // setCenterLon( bb.getCenterX());
  }

  /**
   * Split a latlon rectangle to the equivalent ProjectionRect
   * using this LatLonProjection to split it at the seam if needed.
   *
   * @param latlonR the latlon rectangle to transform
   * @return 1 or 2 ProjectionRect. If it doesnt cross the seam,
   *         the second rectangle is null.
   */
  public ProjectionRect[] latLonToProjRect(LatLonRect latlonR) {

    double lat0 = latlonR.getLowerLeftPoint().getLatitude();
    double height = Math.abs(latlonR.getUpperRightPoint().getLatitude() - lat0);
    double width = latlonR.getWidth();
    double lon0 = LatLonPointImpl.lonNormal(
        latlonR.getLowerLeftPoint().getLongitude(),
        centerLon);
    double lon1 = LatLonPointImpl.lonNormal(
        latlonR.getUpperRightPoint().getLongitude(),
        centerLon);

    ProjectionRect[] rects = new ProjectionRect[] {new ProjectionRect(), new ProjectionRect()};
    if (lon0 < lon1) {
      rects[0].setRect(lon0, lat0, width, height);
      rects[1] = null;

    } else {
      double y = centerLon + 180 - lon0;
      rects[0].setRect(lon0, lat0, y, height);
      rects[1].setRect(lon1 - width + y, lat0, width - y, height);
    }

    return rects;
  }

  public LatLonRect projToLatLonBB(ProjectionRect world) {
    double startLat = world.getMinY();
    double startLon = world.getMinX();

    double deltaLat = world.getHeight();
    double deltaLon = world.getWidth();

    LatLonPoint llpt = new LatLonPointImpl(startLat, startLon);
    return new LatLonRect(llpt, deltaLat, deltaLon);
  }

  /**
   * projection rect 1
   */
  //private ProjectionRect projR1 = new ProjectionRect();

  /**
   * projection rect 1
   */
  //private ProjectionRect projR2 = new ProjectionRect();

  /**
   * array fo rects
   */
  //private ProjectionRect rects[] = new ProjectionRect[2];

  /**
   * Create a latlon rectangle and split it into the equivalent
   * ProjectionRect using this LatLonProjection. The latlon rect is
   * constructed from 2 lat/lon points. The lon values are considered
   * coords in the latlonProjection, and so do not have to be +/- 180.
   *
   * @param lat0 lat of point 1
   * @param lon0 lon of point 1
   * @param lat1 lat of point 1
   * @param lon1 lon of point 1
   * @return 1 or 2 ProjectionRect. If it doesnt cross the seam,
   *         the second rectangle is null.
   */
  public ProjectionRect[] latLonToProjRect(double lat0, double lon0, double lat1, double lon1) {

    double height = Math.abs(lat1 - lat0);
    lat0 = Math.min(lat1, lat0);
    double width = lon1 - lon0;
    if (width < 1.0e-8) {
      width = 360.0;  // assume its the whole thing
    }
    lon0 = LatLonPointImpl.lonNormal(lon0, centerLon);
    lon1 = LatLonPointImpl.lonNormal(lon1, centerLon);

    ProjectionRect[] rects = new ProjectionRect[] {new ProjectionRect(), new ProjectionRect()};
    if (width >= 360.0) {
      rects[0].setRect(centerLon - 180.0, lat0, 360.0, height);
      rects[1] = null;
    } else if (lon0 < lon1) {
      rects[0].setRect(lon0, lat0, width, height);
      rects[1] = null;
    } else {
      double y = centerLon + 180 - lon0;
      rects[0].setRect(lon0, lat0, y, height);
      rects[1].setRect(lon1 - width + y, lat0, width - y, height);
    }
    return rects;
  }

}


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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import thredds.util.ContentType;
import ucar.nc2.constants.CDM;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.StringTokenizer;

/**
 * Handle the /admin/debug interface
 *
 * @author caron
 * @since 4.0
 */
//
@Controller
@RequestMapping(value = "/admin/debug")
public class AdminDebugController {

  @Autowired
  DebugCommands debugCommands;

  @Autowired
  HtmlWriting htmlu;

  // LOOK change to ResponseEntity<String>
  @RequestMapping( method = RequestMethod.GET)
  protected void showDebugPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.setContentType(ContentType.html.getContentHeader());
    response.setHeader("Content-Description", "thredds_debug");

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    PrintStream pw = new PrintStream(bos, false, CDM.UTF8);
    pw.println(htmlu.getHtmlDoctypeAndOpenTag());
    pw.println("");
    pw.println("<title>THREDDS Debug</title>");
    pw.println("<meta http-equiv=\"Content-Type\" content=\"text/html\">");
    pw.println(htmlu.getTdsPageCssLink());
    pw.println(htmlu.getGoogleTrackingContent());
    pw.println("");
    pw.println("");
    pw.println(htmlu.getOldStyleHeader());
    pw.println("
Show TDS Logs");
    pw.println("
Show TDM Logs");
    pw.println("
Show Tomcat Logs");
    pw.println("
Show Config Catalogs");
    pw.println("
Show Spring Controllers");
    pw.println("
Debug Actions
");
    pw.println("
");

    String cmds = request.getQueryString();
    if ((cmds == null) || (cmds.length() == 0)) {
      showDebugActions(request, pw);

    } else {

      StringTokenizer tz = new StringTokenizer(cmds, ";");
      while (tz.hasMoreTokens()) {
        String cmd = tz.nextToken();
        String target = null;

        pw.println("Cmd= " + cmd);
        int pos = cmd.indexOf('/');
        String dhName = "General";
        if (pos > 0) {
          dhName = cmd.substring(0, pos);
          cmd = cmd.substring(pos + 1);
        }

        pos = cmd.indexOf('=');
        if (pos >= 0) {
          target = cmd.substring(pos + 1);
          cmd = cmd.substring(0, pos);
        }

        DebugCommands.Category dh = debugCommands.findCategory(dhName);
        if (dh == null) {
          pw.println(" Unknown Debug Category=" + dhName + "=");
        } else {
          DebugCommands.Action action = dh.actions.get(cmd);
          if (action == null)
            pw.println(" Unknown action=" + cmd + "=");
          else
            action.doAction(new DebugCommands.Event(request, response, pw, bos, target));
        }
      }
    }
    pw.println("
");

    response.setStatus(HttpServletResponse.SC_OK);

    // send it out
    PrintWriter responsePS = response.getWriter();
    responsePS.write(bos.toString(CDM.UTF8));
    responsePS.flush();
  }

  private void showDebugActions(HttpServletRequest req, PrintStream pw) {
    for (DebugCommands.Category dh : debugCommands.getCategories()) {
      pw.println("
" + dh.name + "
");

      for (DebugCommands.Action act : dh.actions.values()) {
        if (act.desc == null) continue;

        String url = req.getRequestURI() + "?" + dh.name + "/" + act.name;
        pw.println("   " + act.desc + "");
      }
    }
  }

}


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

package thredds.server.cdmremote;

import org.jdom2.Document;
import org.jdom2.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import thredds.core.AllowedServices;
import thredds.core.StandardService;
import thredds.core.TdsRequestedDataset;
import thredds.server.config.TdsContext;
import thredds.server.exception.ServiceNotAllowed;
import thredds.servlet.ServletUtil;
import thredds.util.ContentType;
import thredds.util.TdsPathUtils;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.NetcdfFile;
import ucar.nc2.ParsedSectionSpec;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.ft.FeatureDatasetFactoryManager;
import ucar.nc2.stream.NcStreamWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.io.OutputStream;
import java.util.StringTokenizer;

/**
 * Spring controller for CdmRemote service.
 *
 * @author caron
 * @since May 28, 2009
 */
@Controller
@RequestMapping("/cdmremote")
public class CdmRemoteController {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CdmRemoteController.class);
  private static final boolean debug = false, showReq = false;

  @Autowired
  TdsContext tdsContext;

  @Autowired
  private AllowedServices allowedServices;

  @InitBinder("CdmRemoteQueryBean")
  protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new CdmRemoteQueryBeanValidator());
  }

  // everything but header, data, which is binary data, and capabilities which is XML
  @RequestMapping(value = "/**", method = RequestMethod.GET)
  public ResponseEntity<String> handleCapabilitiesRequest(HttpServletRequest request, HttpServletResponse response, @RequestParam String req) throws IOException {

    if (!allowedServices.isAllowed(StandardService.cdmRemote))
      throw new ServiceNotAllowed(StandardService.cdmRemote.toString());

    String datasetPath = TdsPathUtils.extractPath(request, "/cdmremote");
    String absPath = getAbsolutePath(request);
    HttpHeaders responseHeaders;

    if (showReq)
      System.out.printf("CdmRemoteController req=%s%n", absPath + "?" + request.getQueryString());
    if (debug)
      System.out.printf(" path=%s%n query=%s%n", datasetPath, request.getQueryString());

    // LOOK heres where we want the Dataset, not the netcdfFile (!)
    try (NetcdfFile ncfile = TdsRequestedDataset.getNetcdfFile(request, response, datasetPath)) {
      if (ncfile == null) return null;  // failed resource control

      responseHeaders = new HttpHeaders();
      responseHeaders.setDate("Last-Modified", TdsRequestedDataset.getLastModified(datasetPath));

      // a request without a parameter is a test to see if this is a valid cdremote endpoint.
      // just setHeader("Content-Description", "ncstream"), no body
      // on client, see DatasetUrl.disambiguateHttp
      if (req == null) {
          response.setContentType(ContentType.binary.getContentHeader());
          response.setHeader("Content-Description", "ncstream");
          return new ResponseEntity<>(null, responseHeaders, HttpStatus.OK);
        }

      switch (req.toLowerCase()) {
        case "form":    // ol
        case "cdl":
          ncfile.setLocation(datasetPath); // hide where the file is stored  LOOK
          String cdl = ncfile.toString();
          responseHeaders.set(ContentType.HEADER, ContentType.text.getContentHeader());
          return new ResponseEntity<>(cdl, responseHeaders, HttpStatus.OK);

        case "ncml":
          String ncml = ncfile.toNcML(absPath);
          responseHeaders.set(ContentType.HEADER, ContentType.xml.getContentHeader());
          return new ResponseEntity<>(ncml, responseHeaders, HttpStatus.OK);

        default:
          return new ResponseEntity<>("Unrecognized request", null, HttpStatus.BAD_REQUEST);
      }
    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=capabilities")
  public ModelAndView handleCapabilitiesRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {

    if (!allowedServices.isAllowed(StandardService.cdmRemote))
      throw new ServiceNotAllowed(StandardService.cdmRemote.toString());

    String datasetPath = TdsPathUtils.extractPath(request, "/cdmremote");
    String absPath = getAbsolutePath(request);

    try (NetcdfFile ncfile = TdsRequestedDataset.getNetcdfFile(request, response, datasetPath)) {
      if (ncfile == null) return null;

      Element rootElem = new Element("cdmRemoteCapabilities");
      Document doc = new Document(rootElem);
      rootElem.setAttribute("location", absPath);

      Element elem = new Element("featureDataset");
      FeatureType ftFromMetadata = FeatureDatasetFactoryManager.findFeatureType(ncfile); // LOOK BAD - must figure out what is the featureType and save it
      if (ftFromMetadata != null)
        elem.setAttribute("type", ftFromMetadata.toString());
      elem.setAttribute("url", absPath);
      rootElem.addContent(elem);

      return new ModelAndView("threddsXmlView", "Document", doc);
    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=header")
  public void handleHeaderRequest(HttpServletRequest request, HttpServletResponse response, OutputStream out) throws IOException {

    if (!allowedServices.isAllowed(StandardService.cdmRemote))
      throw new ServiceNotAllowed(StandardService.cdmRemote.toString());

    String datasetPath = TdsPathUtils.extractPath(request, "/cdmremote");
    String absPath = getAbsolutePath(request);

    if (showReq)
      System.out.printf("CdmRemoteController req=%s%n", absPath + "?" + request.getQueryString());
    if (debug) {
      System.out.printf(" path=%s%n query=%s%n", datasetPath, request.getQueryString());
    }

    try (NetcdfFile ncfile = TdsRequestedDataset.getNetcdfFile(request, response, datasetPath)) {
      if (ncfile == null) return;

      response.setContentType(ContentType.binary.getContentHeader());
      response.setHeader("Content-Description", "ncstream");
      response.addDateHeader("Last-Modified", TdsRequestedDataset.getLastModified(datasetPath));

      NcStreamWriter ncWriter = new NcStreamWriter(ncfile, ServletUtil.getRequestBase(request));
      long size = ncWriter.sendHeader(out);
      out.flush();

      if (debug)
        System.out.printf("CdmRemoteController header ok, size=%s%n", size);
    }
  }

  @RequestMapping(value = "/**", method = RequestMethod.GET, params = "req=data")
  public void handleDataRequest(HttpServletRequest request, HttpServletResponse response,
                            @Valid CdmRemoteQueryBean qb, BindingResult validationResult, OutputStream out)
          throws IOException, BindException, InvalidRangeException {

    if (!allowedServices.isAllowed(StandardService.cdmRemote))
      throw new ServiceNotAllowed(StandardService.cdmRemote.toString());

    if (validationResult.hasErrors())
      throw new BindException(validationResult);

    String datasetPath = TdsPathUtils.extractPath(request, "/cdmremote");
    String absPath = getAbsolutePath(request);

    if (showReq)
      System.out.printf("CdmRemoteController req=%s%n", absPath + "?" + request.getQueryString());
    if (debug)
      System.out.printf(" path=%s%n query=%s%n", datasetPath, request.getQueryString());
    long start = System.currentTimeMillis();

    try (NetcdfFile ncfile = TdsRequestedDataset.getNetcdfFile(request, response, datasetPath)) {
      if (ncfile == null) return;

      response.setContentType(ContentType.binary.getContentHeader());
      response.setHeader("Content-Description", "ncstream");

      long size = 0;
      //WritableByteChannel wbc = Channels.newChannel(out);
      NcStreamWriter ncWriter = new NcStreamWriter(ncfile, ServletUtil.getRequestBase(request));
      String query;
      if (qb.getVar() != null)
        query = qb.getVar();
      else
        query = request.getQueryString(); // LOOK ??

      if ((query == null) || (query.length() == 0)) {
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "must have query string");
        return;
      }

      // query = EscapeStrings.unescapeURLQuery(query);
      StringTokenizer stoke = new StringTokenizer(query, ";"); // need UTF/%decode
      while (stoke.hasMoreTokens()) {
        ParsedSectionSpec cer = ParsedSectionSpec.parseVariableSection(ncfile, stoke.nextToken());
        // size += ncWriter.sendData(cer.v, cer.section, out, qb.getCompression());
        size += ncWriter.sendData2(cer.v, cer.section, out, qb.getCompression());
        // size += ncWriter.sendData3(cer.v, cer.section, out, qb.getCompression());
      }
      out.flush();

      if (debug)
        System.out.printf("CdmRemoteController data ok, size=%s took=%d%n", size, System.currentTimeMillis() - start);

    } //catch (IllegalArgumentException | InvalidRangeException e) { // ParsedSectionSpec failed
     // response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
    // }

  }

  private String getAbsolutePath(HttpServletRequest req) {
    return ServletUtil.getRequestServer(req) + req.getContextPath() + req.getServletPath();
  }

}


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

/**
 * Specialization of Index for rank 5 arrays.
 *
 * @see Index
 * @author caron
 */
public class Index5D extends Index {

  /** current element's indices */
  private int curr0, curr1, curr2, curr3, curr4;
  /** array strides */
  private int stride0, stride1, stride2, stride3, stride4;
  /** array shapes */
  private int shape0, shape1, shape2, shape3, shape4;

  Index5D() { super(5); }
  public Index5D( int[] shape) {
    super(shape);
    precalc();
  }

  protected void precalc() {
    shape0 = shape[0];
    shape1 = shape[1];
    shape2 = shape[2];
    shape3 = shape[3];
    shape4 = shape[4];

    stride0 = stride[0];
    stride1 = stride[1];
    stride2 = stride[2];
    stride3 = stride[3];
    stride4 = stride[4];

    curr0 = current[0];
    curr1 = current[1];
    curr2 = current[2];
    curr3 = current[3];
    curr4 = current[4];
  }

   public String toString() {
     return curr0+","+curr1+","+curr2+","+curr3+","+curr4;
   }

  public int [] getCurrentCounter() {
    current[0] = curr0;
    current[1] = curr1;
    current[2] = curr2;
    current[3] = curr3;
    current[4] = curr4;
    return current.clone();
  }

  public int currentElement() {
    return offset + curr0*stride0 + curr1*stride1 + curr2*stride2 +
        + curr3*stride3 + curr4*stride4;
  }

  public int incr() {
        if (++curr4 >= shape4) {
          curr4 = 0;
          if (++curr3 >= shape3) {
            curr3 = 0;
              if (++curr2 >= shape2) {
                curr2 = 0;
                if (++curr1 >= shape1) {
                  curr1 = 0;
                  if (++curr0 >= shape0) {
                    curr0 = 0;    // rollover !
                  }
                }
              }
            }
          }

    return offset + curr0*stride0 + curr1*stride1 + curr2*stride2 +
        + curr3*stride3 + curr4*stride4;
  }

  public void setDim(int dim, int value) {
    if (value < 0 || value >= shape[dim])  // check index here
      throw new ArrayIndexOutOfBoundsException();

    if (dim == 4)
      curr4 = value;
    else if (dim == 3)
      curr3 = value;
    else if (dim == 2)
      curr2 = value;
    else if (dim == 1)
      curr1 = value;
    else
      curr0 = value;
  }

  public Index set0(int v) {
    if (v < 0 || v >= shape0)  // check index here
      throw new ArrayIndexOutOfBoundsException();
    curr0 = v;
    return this;
  }

  public Index set1(int v) {
    if (v < 0 || v >= shape1)  // check index here
      throw new ArrayIndexOutOfBoundsException();
    curr1 = v;
    return this;
  }

  public Index set2(int v) {
    if (v < 0 || v >= shape2)  // check index here
      throw new ArrayIndexOutOfBoundsException();
    curr2 = v;
    return this;
  }

  public Index set3(int v) {
    if (v < 0 || v >= shape3)  // check index here
      throw new ArrayIndexOutOfBoundsException();
    curr3 = v;
    return this;
  }

  public Index set4(int v) {
    if (v < 0 || v >= shape4)  // check index here
      throw new ArrayIndexOutOfBoundsException();
    curr4 = v;
    return this;
  }

  public Index set(int[] index){
    if (index.length != rank)
      throw new ArrayIndexOutOfBoundsException();
    set0(index[0]);
    set1(index[1]);
    set2(index[2]);
    set3(index[3]);
    set4(index[4]);
    return this;
  }

  public Index set(int v0, int v1, int v2, int v3, int v4) {
    set0(v0);
    set1(v1);
    set2(v2);
    set3(v3);
    set4(v4);
    return this;
  }

  public Object clone() {
    return super.clone();
  }

    //experimental : should be package private
  int setDirect(int v0, int v1, int v2, int v3, int v4) {
    return offset + v0*stride0 + v1*stride1 + v2*stride2 + v3*stride3 +
        v4*stride4;
  }

}