/* Copyright */
package thredds.server;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import thredds.server.exception.RequestTooLargeException;
import thredds.server.exception.ServiceNotAllowed;
import thredds.server.ncss.exception.NcssException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Formatter;
import java.util.List;

import static org.springframework.web.util.HtmlUtils.htmlEscape;

/**
 * Global Exception handling
 *   ServiceNotAllowed                      FORBIDDEN
 *   FileNotFoundException                  NOT_FOUND
 *   IOException                            INTERNAL_SERVER_ERROR
 *   UnsupportedOperationException          BAD_REQUEST
 *   IllegalArgumentException               BAD_REQUEST
 *   BindException                          BAD_REQUEST
 *   Throwable                              INTERNAL_SERVER_ERROR
 *
 * @author caron
 * @see "https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc"
 * @since 4/15/2015
 */
@Configuration
@ControllerAdvice
public class TdsErrorHandling implements HandlerExceptionResolver {
  private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TdsErrorHandling.class);

  @ExceptionHandler(ServiceNotAllowed.class)
  public ResponseEntity<String> handle(ServiceNotAllowed ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>("Service Not Allowed: " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.FORBIDDEN);
  }

  @ExceptionHandler(RequestTooLargeException.class)
  public ResponseEntity<String> handle(RequestTooLargeException ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>("Request Too Large: " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.FORBIDDEN);
  }

  @ExceptionHandler(FileNotFoundException.class)
  public ResponseEntity<String> handle(FileNotFoundException ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>("FileNotFound: " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.NOT_FOUND);
  }

  @ExceptionHandler(IOException.class)
  public ResponseEntity<String> handle(IOException ex) {
    String eName = ex.getClass().getName(); // dont want compile time dependency on ClientAbortException
    if (eName.equals("org.apache.catalina.connector.ClientAbortException")) {
      logger.debug("ClientAbortException while sending file: ", ex);
      return null;
    }

    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>(
            "IOException sending File " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.INTERNAL_SERVER_ERROR);
  }

  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<String> handle(IllegalArgumentException ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    String mess = ex.getMessage();

    if (mess != null && mess.startsWith("RequestTooLarge")) // RequestTooLargeException only avail in tds module
      return new ResponseEntity<>("Request Too Large: " + htmlEscape(mess), responseHeaders, HttpStatus.FORBIDDEN);
    else
      return new ResponseEntity<>("IllegalArgumentException: " + htmlEscape(mess), responseHeaders, HttpStatus.BAD_REQUEST);
  }

  @ExceptionHandler(NcssException.class)
  public ResponseEntity<String> handle(NcssException ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>("Invalid Request: " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.BAD_REQUEST);
  }

  @ExceptionHandler(org.springframework.web.bind.ServletRequestBindingException.class)
  public ResponseEntity<String> handle(org.springframework.web.bind.ServletRequestBindingException ex) {
    logger.warn("TDS Error", ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>("Invalid Request: " + htmlEscape(ex.getMessage()), responseHeaders, HttpStatus.BAD_REQUEST);
  }

  @ExceptionHandler(BindException.class)
  public ResponseEntity<String> handle(BindException ex) {
    BindingResult validationResult = ex.getBindingResult();
    List<ObjectError> errors = validationResult.getAllErrors();
    Formatter f = new Formatter();
    f.format("Validation errors: ");
    for (ObjectError err : errors) {
      f.format(" %s%n", err.getDefaultMessage());
    }

    logger.warn(f.toString(), ex);

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    return new ResponseEntity<>(f.toString(), responseHeaders, HttpStatus.BAD_REQUEST);
  }

  // LOOK this could be a problem
  @ExceptionHandler(Throwable.class)
  public ResponseEntity<String> handle(Throwable ex) throws Throwable {
    // If the exception is annotated with @ResponseStatus rethrow it and let
    // the framework handle it - like the OrderNotFoundException example
    // at the start of this post.
    // AnnotationUtils is a Spring Framework utility class.
    // see https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc
    if (AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class) != null)
      throw ex;

    logger.error("uncaught exception", ex);
    // ex.printStackTrace(); // temporary - remove in production

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.setContentType(MediaType.TEXT_PLAIN);
    String msg = ex.getMessage();
      StringWriter sw = new StringWriter();
      PrintWriter p = new PrintWriter(sw);
      ex.printStackTrace(p);
      p.close();
      sw.close();
      msg = sw.toString();
    return new ResponseEntity<>("Throwable exception handled : " + htmlEscape(msg), responseHeaders, HttpStatus.INTERNAL_SERVER_ERROR);
  }

  /////////////////////////////////////////////
  /// this catches exception from everything else, eg views

  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    logger.error("uncaught exception 2", ex);
    return null;
  }

  /*
  see http://www.mytechnotes.biz/2012/08/spring-mvc-with-annotations-example.html

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;

import java.util.ArrayList;
import java.util.List;
    @Bean
    HandlerExceptionResolverComposite getHandlerExceptionResolverComposite() {

      HandlerExceptionResolverComposite result = new HandlerExceptionResolverComposite();

      List<HandlerExceptionResolver> l = new ArrayList<>();

        l.add(new AnnotationMethodHandlerExceptionResolver());
        l.add(new ResponseStatusExceptionResolver());
        l.add(getSimpleMappingExceptionResolver());
        l.add(new DefaultHandlerExceptionResolver());

      result.setExceptionResolvers(l);

      return result;
    }      */

}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information. */

package dap4.core.util;

import dap4.core.dmr.DapDimension;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * A classic implementation of an odometer
 * taken from the netcdf-c code.
 * Extended to provide iterator interface
 */

public class Odometer implements Iterator<Index>
{

    //////////////////////////////////////////////////
    // Constants

    // Mnemonic

    static protected enum STATE
    {
        INITIAL, STARTED, DONE;
    }

    //////////////////////////////////////////////////
    // factories

    static public Odometer
    factoryScalar()
    {
        return new ScalarOdometer();
    }

    static public Odometer
    factory(List<Slice> slices)
            throws DapException
    {
        return factory(slices, null);
    }

    static public Odometer
    factory(List<Slice> slices, List<DapDimension> dimset)
            throws DapException
    {
        // check for scalar case
        if(dimset != null && dimset.size() == 0) {
            if(!DapUtil.isScalarSlices(slices))
                throw new DapException("Cannot build scalar odometer with non-scalar slices");
            return factoryScalar();
        }
        boolean multi = false;
        if(slices != null) {
            for(int i = 0; i < slices.size(); i++) {
                if(slices.get(i).getSort() == Slice.Sort.Multi) {
                    multi = true;
                    break;
                }
            }
        }
        if(slices == null || slices.size() == 0)
            return factoryScalar();
        else if(multi)
            return new MultiOdometer(slices, dimset);
        else
            return new Odometer(slices, dimset);
    }

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

    protected STATE state = STATE.INITIAL;

    protected boolean ismulti = false;

    protected int rank = 0;
    protected List<Slice> slices = null;
    protected List<DapDimension> dimset = null;

    // The current odometer indices
    protected Index index;

    // precompute this.slices[i].getLast() - this.slices[i].getStride()
    protected long[] endpoint;

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

    public Odometer()
    {
    }

    public Odometer(List<Slice> set)
            throws DapException
    {
        this(set, null);
    }

    public Odometer(List<Slice> set, List<DapDimension> dimset)
            throws DapException
    {
        if(set == null)
            throw new DapException("Null slice list");
        if(dimset != null && set.size() != dimset.size())
            throw new DapException("Rank mismatch");
        this.rank = set.size();
        if(this.rank == 0)
            throw new DapException("Rank == 0; use Scalar Odometer");
        this.slices = new ArrayList<>();
        this.slices.addAll(set);
        if(dimset != null) {
            this.dimset = new ArrayList<>();
            this.dimset.addAll(dimset);
        }
        this.endpoint = new long[this.rank];
        this.index = new Index(rank);
        for(int i = 0; i < this.rank; i++) {
            this.index.dimsizes[i] = slices.get(i).getMax();
        }
        reset();
    }

    protected void
    reset()
    {
        for(int i = 0; i < this.rank; i++) {
            try {
                slices.get(i).finish();
                this.index.indices[i] = this.slices.get(i).getFirst();
                this.endpoint[i] = this.slices.get(i).getLast() - this.slices.get(i).getStride();
            } catch (DapException de) {
                throw new IllegalArgumentException(de);
            }
        }
    }

    public String toString()
    {
        StringBuilder buf = new StringBuilder();
        for(int i = 0; i < rank; i++) {
            if(i > 0)
                buf.append(",");
            if(dimset != null)
                buf.append(dimset.get(i) != null ? dimset.get(i).getShortName() : "null");
            buf.append(this.slices.get(i).toString());
            buf.append(String.format("(%d)", this.slices.get(i).getCount()));
            if(this.index != null)
                buf.append(String.format("@%d", this.index.indices[i]));
        }
        return buf.toString();
    }

    //////////////////////////////////////////////////
    // Odometer API

    /**
     * Return odometer rank
     */
    public int
    rank()
    {
        return this.rank;
    }

    /**
     * Return ith slice
     */
    public Slice
    slice(int i)
    {
        if(i < 0 || i >= this.rank)
            throw new IllegalArgumentException();
        return this.slices.get(i);
    }

    public List<Slice>
    getSlices()
    {
        return this.slices;
    }

    /**
     * Compute the linear index
     * from the current odometer indices.
     */
    public long
    index()
    {
        return index.index();
    }

    /**
     * Return current set of indices
     */
    public Index
    indices()
    {
        return this.index;
    }

    /**
     * Compute the total number of elements.
     */
    public long
    totalSize()
    {
        long size = 1;
        for(int i = 0; i < this.rank; i++) {
            size *= this.slices.get(i).getCount();
        }
        return size;
    }

    //////////////////////////////////////////////////
    // Iterator API

    @Override
    public boolean
    hasNext()
    {
        int stop = this.rank;
        switch (this.state) {
        case INITIAL:
            return true;
        case STARTED:
            int i;
            for(i = stop - 1; i >= 0; i--) { // walk backwards
                if(this.index.indices[i] <= this.endpoint[i])
                    return true;
            }
            this.state = STATE.DONE;
            break;
        case DONE:
        }
        return false;
    }

    @Override
    public Index
    next()
    {
        int i;
        int lastpos = this.rank;
        int firstpos = 0;
        switch (this.state) {
        case INITIAL:
            this.state = STATE.STARTED;
            break;
        case STARTED:
            i = step(firstpos, lastpos);
            if(i < 0)
                this.state = STATE.DONE;
            break;
        case DONE:
            break;
        }
        if(this.state == STATE.DONE)
            throw new NoSuchElementException();
        return indices();
    }

    @Override
    public void remove()
    {
        throw new UnsupportedOperationException();
    }

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

    // on entry: indices are the last index set
    // on exit, the indices are the next value
    // return index of place where we have room to step;
    // return -1 if we have completed.
    public int
    step(int firstpos, int lastpos)
    {
        for(int i = lastpos - 1; i >= firstpos; i--) { // walk backwards
            if(this.index.indices[i] > this.endpoint[i])
                this.index.indices[i] = this.slices.get(i).getFirst(); // reset this position
            else {
                this.index.indices[i] += this.slices.get(i).getStride();  // move to next indices
                return i;
            }
        }
        return -1;
    }

    public List<Odometer>
    getSubOdometers()
    {
        List<Odometer> list = new ArrayList<>();
        list.add(this);
        return list;
    }

    public boolean
    isMulti()
    {
        return this.ismulti;
    }

}


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

package ucar.nc2.grib.grib1.tables;

import javax.annotation.Nullable;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import ucar.nc2.grib.GribResourceReader;
import ucar.nc2.grib.GribStatType;
import ucar.nc2.grib.coord.VertCoordType;
import ucar.nc2.grib.grib1.Grib1ParamTime;
import ucar.nc2.grib.grib1.Grib1SectionProductDefinition;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * NCEP overrides of GRIB tables
 * LOOK: Why not a singleton?
 *
 * @author caron
 * @since 1/13/12
 */
public class NcepTables extends Grib1Customizer {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(NcepTables.class);

  private static Map<Integer, String> genProcessMap;  // shared by all instances
  private static Map<Integer, VertCoordType> levelTypesMap;  // shared by all instances

  NcepTables(Grib1ParamTables tables) {
    super(7, tables);
  }

  protected NcepTables(int center, Grib1ParamTables tables) {
    super(center, tables);
  }

  ////////////// time types ////////////////////////////////////
  // http://www.nco.ncep.noaa.gov/pmb/docs/on388/table5.html
  @Override
  public GribStatType getStatType(int timeRangeIndicator) {

    switch (timeRangeIndicator) {
      case 128:
      case 129:
      case 130:
      case 131:
      case 132:
      case 133:
      case 137:
      case 138:
      case 139:
      case 140:
        return GribStatType.Average;
      case 134:
        return GribStatType.RootMeanSquare;
      case 135:
      case 136:
        return GribStatType.StandardDeviation;
      default:
        return super.getStatType(timeRangeIndicator);
    }
  }

    /////////////////// local time types
  @Override
  public Grib1ParamTime getParamTime(Grib1SectionProductDefinition pds) {
    int p1 = pds.getTimeValue1();  // octet 19
    int p2 = pds.getTimeValue2();  // octet 20
    int timeRangeIndicator = pds.getTimeRangeIndicator(); // octet 21
    int n = pds.getNincluded();

    int start;
    int end;
    int forecastTime = 0;
    boolean isInterval;

    switch (timeRangeIndicator) {

      case 128:
      case 129:
      case 130:
      case 131:
      case 137:
      case 138:
      case 139:
      case 140:
        isInterval = true;
        start = p1;
        end = p2;
        break;

      case 132:
      case 133:
      case 134:
      case 135:
      case 136:
        forecastTime = p1;
        start = p1;
        end = (n > 0) ? p1 + (n-1) * p2 : p1;  // LOOK ??
        isInterval = (n > 0);
        break;

      default:
        return super.getParamTime(pds);
    }

    return new Grib1ParamTime(this, timeRangeIndicator, isInterval, start, end, forecastTime);
  }

  @Override
  public String getTimeTypeName(int timeRangeIndicator) {

    switch (timeRangeIndicator) {
      case 128:
        /* Average of forecast accumulations. P1 = start of accumulation period. P2 = end of accumulation period.
          Reference time is the start time of the first forecast, other forecasts at 24-hour intervals.
          Number in Ave = number of forecasts used. */
        return "Average of forecast accumulations at 24 hour intervals, period = (RT + P1) to (RT + P2)";

      case 129:
        /* Average of successive forecast accumulations. P1 = start of accumulation period.
        P2 = end of accumulation period. Reference time is the start time of the first forecast,
        other forecasts at (P2 - P1) intervals. Number in Ave = number of forecasts used */
        return "Average of successive forecast accumulations, period = (RT + P1) to (RT + P2)";

      case 130:
        /* Average of forecast averages. P1 = start of averaging period. P2 = end of averaging period.
        Reference time is the start time of the first forecast, other forecasts at 24-hour intervals.
        Number in Ave = number of forecast used*/
        return "Average of forecast averages at 24 hour intervals, period = (RT + P1) to (RT + P2)";

      case 131:
        /* Average of successive forecast averages. P1 = start of averaging period. P2 = end of averaging period.
        Reference time is the start time of the first forecast, other forecasts at (P2 - P1) intervals.
        Number in Ave = number of forecasts used*/
        return "Average of successive forecast averages, period = (RT + P1) to (RT + P2)";

      case 132:
        return "Climatological Average of N analyses, each a year apart, starting from initial time R and for the period from R+P1 to R+P2.";

      case 133:
        return "Climatological Average of N forecasts, each a year apart, starting from initial time R and for the period from R+P1 to R+P2.";

      case 134:
        return "Climatological Root Mean Square difference between N forecasts and their verifying analyses, each a year apart, starting with initial time R and for the period from R+P1 to R+P2.";

      case 135:
        return "Climatological Standard Deviation of N forecasts from the mean of the same N forecasts, for forecasts one year apart. ";

      case 136:
        return "Climatological Standard Deviation of N analyses from the mean of the same N analyses, for analyses one year apart.";

      case 137:
        /* Average of forecast accumulations. P1 = start of accumulation period. P2 = end of accumulation period.
        Reference time is the start time of the first forecast, other forecasts at 6-hour intervals.
        Number in Ave = number of forecast used */
        return "Average of forecast accumulations at 6 hour intervals, period = (RT + P1) to (RT + P2)";

      case 138:
        /* Average of forecast averages. P1 = start of averaging period. P2 = end of averaging period.
        Reference time is the start time of the first forecast, other forecasts at 6-hour intervals.
        Number in Ave = number of forecast used */
        return "Average of forecast averages at 6 hour intervals, period = (RT + P1) to (RT + P2)";

      case 139:
        /* Average of forecast accumulations. P1 = start of accumulation period. P2 = end of accumulation period.
        Reference time is the start time of the first forecast, other forecasts at 12-hour intervals.
        Number in Ave = number of forecast used */
        return "Average of forecast accumulations at 12 hour intervals, period = (RT + P1) to (RT + P2)";

      case 140:
        /* Average of forecast averages. P1 = start of averaging period. P2 = end of averaging period.
        Reference time is the start time of the first forecast, other forecasts at 12-hour intervals.
        Number in Ave = number of forecast used */
        return "Average of forecast averages at 12 hour intervals, period = (RT + P1) to (RT + P2)";

      default:
        return super.getTimeTypeName(timeRangeIndicator);
    }
  }

  //////////////////////////////////////////// genProcess

  @Override
  @Nullable
  public String getGeneratingProcessName(int genProcess) {
    if (genProcessMap == null)
        genProcessMap = getNcepGenProcess();
    if (genProcessMap == null) return null;

    return genProcessMap.get(genProcess);
  }

  // public so can be called from Grib2
  @Nullable
  public static Map<Integer, String> getNcepGenProcess() {
    if (genProcessMap != null) return genProcessMap;
    String path = "resources/grib1/ncep/ncepTableA.xml";
    try (InputStream is = GribResourceReader.getInputStream(path)) {
      SAXBuilder builder = new SAXBuilder();
      org.jdom2.Document doc = builder.build(is);
      Element root = doc.getRootElement();

      HashMap<Integer, String> result = new HashMap<>(200);
      List<Element> params = root.getChildren("parameter");
      for (Element elem1 : params) {
        int code = Integer.parseInt(elem1.getAttributeValue("code"));
        String desc = elem1.getChildText("description");
        result.put(code, desc);
      }
      return Collections.unmodifiableMap(result);  // all at once - thread safe

    } catch (IOException | JDOMException ioe) {
      logger.error("Cant read NCEP Table 1 = " + path, ioe);
      return null;
    }
  }

  ///////////////////////////////////////// levels
  protected VertCoordType getLevelType(int code) {
    if (code < 129)
      return super.getLevelType(code); // LOOK dont let NCEP override standard tables (??) looks like a conflict with level code 210 (!)

    if (levelTypesMap == null)
      levelTypesMap = readTable3("resources/grib1/ncep/ncepTable3.xml");
    if (levelTypesMap == null)
      return super.getLevelType(code);

    VertCoordType levelType = levelTypesMap.get(code);
    if (levelType != null) return levelType;

    return super.getLevelType(code);
  }

}


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

package ucar.nc2.grib.collection;

import com.google.protobuf.ByteString;
import thredds.inventory.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.grib.coord.Coordinate;
import ucar.nc2.grib.coord.CoordinateEns;
import ucar.nc2.grib.coord.CoordinateRuntime;
import ucar.nc2.grib.coord.CoordinateTime;
import ucar.nc2.grib.coord.CoordinateTime2D;
import ucar.nc2.grib.coord.CoordinateTimeIntv;
import ucar.nc2.grib.coord.CoordinateVert;
import ucar.nc2.grib.coord.SparseArray;
import ucar.nc2.grib.grib2.*;
import ucar.nc2.stream.NcStream;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.unidata.io.RandomAccessFile;

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

/**
 * Build a GribCollection object for Grib-2 files. Manage grib collection index.
 * Covers GribCollectionProto, which serializes and deserializes.
 *
 * @author caron
 * @since 4/6/11
 */
class Grib2CollectionWriter extends GribCollectionWriter {
  public static final String MAGIC_START = "Grib2Collectio2Index";  // was Grib2CollectionIndex
  static final int minVersion = 1;  // increment this when you want to force index rebuild
  protected static final int version = 3;     // increment this as needed, must be backwards compatible through minVersion

  Grib2CollectionWriter(MCollection dcm, org.slf4j.Logger logger) {
    super(dcm, logger);
  }

  public static class Group implements GribCollectionBuilder.Group {
    public final Grib2SectionGridDefinition gdss;
    public final int hashCode;       // may have been modified
    public CalendarDate runtime;

    List<Grib2CollectionBuilder.VariableBag> gribVars;
    public List<Coordinate> coords;
    public final List<Grib2Record> records = new ArrayList<>();
    public final Set<Long> runtimes = new HashSet<>();
    Set<Integer> fileSet; // this is so we can show just the component files that are in this group

    Group(Grib2SectionGridDefinition gdss, int hashCode) {
      this.gdss = gdss;
      this.hashCode = hashCode;
    }

    Group(Grib2SectionGridDefinition gdss, int hashCode, CalendarDate runtime) {
      this.gdss = gdss;
      this.hashCode = hashCode;
      this.runtime = runtime;
    }

    @Override
    public CalendarDate getRuntime() {
      return runtime;
    }

    @Override
    public Set<Long> getCoordinateRuntimes() {
      return runtimes;
    }

    @Override
    public List<Coordinate> getCoordinates() {
      return coords;
    }
  }

  ///////////////////////////////////////////////////
  // heres where the actual writing is

  /*
   MAGIC_START
   version
   sizeRecords
   SparseArray's (sizeRecords bytes)
   sizeIndex
   GribCollectionIndex (sizeIndex bytes)
   */

  boolean writeIndex(String name, File idxFile, CoordinateRuntime masterRuntime, List<Group> groups, List<MFile> files,
                            GribCollectionImmutable.Type type, CalendarDateRange dateRange) throws IOException {
    Grib2Record first = null; // take global metadata from here
    boolean deleteOnClose = false;

    if (idxFile.exists()) {
      RandomAccessFile.eject(idxFile.getPath());
      if (!idxFile.delete()) {
        logger.error("gc2 cant delete index file {}", idxFile.getPath());
      }
    }
    logger.debug(" createIndex for {}", idxFile.getPath());

    try (RandomAccessFile raf = new RandomAccessFile(idxFile.getPath(), "rw")) {
      //// header message
      raf.order(RandomAccessFile.BIG_ENDIAN);
      raf.write(MAGIC_START.getBytes(CDM.utf8Charset));
      raf.writeInt(version);
      long lenPos = raf.getFilePointer();
      raf.writeLong(0); // save space to write the length of the record section
      long countBytes = 0;
      int countRecords = 0;

      Set<Integer> allFileSet = new HashSet<>();
      for (Group g : groups) {
        g.fileSet = new HashSet<>();
        for (Grib2CollectionBuilder.VariableBag vb : g.gribVars) {
          if (first == null) first = vb.first;
          GribCollectionProto.SparseArray vr = writeSparseArray(vb, g.fileSet);
          byte[] b = vr.toByteArray();
          vb.pos = raf.getFilePointer();
          vb.length = b.length;
          raf.write(b);
          countBytes += b.length;
          countRecords += vb.coordND.getSparseArray().countNotMissing();
        }
        allFileSet.addAll(g.fileSet);
      }

      if (logger.isDebugEnabled()) {
        long bytesPerRecord = countBytes / ((countRecords == 0) ? 1 : countRecords);
        logger.debug("  write RecordMaps: bytes = {} record = {} bytesPerRecord={}", countBytes, countRecords, bytesPerRecord);
      }

      if (first == null) {
        deleteOnClose = true;
        throw new IOException("GribCollection " + name + " has no records");
      }

      long pos = raf.getFilePointer();
      raf.seek(lenPos);
      raf.writeLong(countBytes);
      raf.seek(pos); // back to the output.

      /*
      message GribCollection {
        string name = 1;         // must be unique - index filename is name.ncx
        string topDir = 2;       // MFile, Partition filenames are reletive to this
        repeated MFile mfiles = 3;        // list of grib MFiles
        repeated Dataset dataset = 4;
        repeated Gds gds = 5;             // unique Gds, shared amongst datasets
        Coord masterRuntime = 6;  // list of runtimes in this GC

        int32 center = 7;      // these 4 fields are to get a GribCustomizer
        int32 subcenter = 8;
        int32 master = 9;
        int32 local = 10;       // grib1 table Version

        int32 genProcessType = 11;
        int32 genProcessId = 12;
        int32 backProcessId = 13;
        int32 version = 14;     // >= 3 for proto3 (5.0+)

        // repeated Parameter params = 20;      // not used
        FcConfig config = 21;

        // extensions
        repeated Partition partitions = 100;
        bool isPartitionOfPartitions = 101;
        repeated uint32 run2part = 102 [packed=true];  // masterRuntime index to partition index
      }
       */
      GribCollectionProto.GribCollection.Builder indexBuilder = GribCollectionProto.GribCollection.newBuilder();
      indexBuilder.setName(name);
      indexBuilder.setTopDir(dcm.getRoot());
      indexBuilder.setVersion(currentVersion);

      // directory and mfile list
      File directory = new File(dcm.getRoot());
      List<GcMFile> gcmfiles = GcMFile.makeFiles(directory, files, allFileSet);
      for (GcMFile gcmfile : gcmfiles) {
        GribCollectionProto.MFile.Builder b = GribCollectionProto.MFile.newBuilder();
        b.setFilename(gcmfile.getName());
        b.setLastModified(gcmfile.getLastModified());
        b.setLength(gcmfile.getLength());
        b.setIndex(gcmfile.index);
        indexBuilder.addMfiles(b.build());
      }

      indexBuilder.setMasterRuntime(writeCoordProto(masterRuntime));

       //gds
      for (Object go : groups) {
        Group g = (Group) go;
        indexBuilder.addGds(writeGdsProto(g.gdss.getRawBytes(), -1));
      }

      // the GC dataset
      indexBuilder.addDataset(writeDatasetProto(type, groups));

      // what about just storing first ??
      Grib2SectionIdentification ids = first.getId();
      indexBuilder.setCenter(ids.getCenter_id());
      indexBuilder.setSubcenter(ids.getSubcenter_id());
      indexBuilder.setMaster(ids.getMaster_table_version());
      indexBuilder.setLocal(ids.getLocal_table_version());

      Grib2Pds pds = first.getPDS();
      indexBuilder.setGenProcessType(pds.getGenProcessType());
      indexBuilder.setGenProcessId(pds.getGenProcessId());
      indexBuilder.setBackProcessId(pds.getBackProcessId());

      indexBuilder.setStartTime(dateRange.getStart().getMillis());
      indexBuilder.setEndTime(dateRange.getEnd().getMillis());

      GribCollectionProto.GribCollection index = indexBuilder.build();
      byte[] b = index.toByteArray();
      NcStream.writeVInt(raf, b.length); // message size
      raf.write(b);  // message  - all in one gulp
      logger.debug("  write GribCollectionIndex= {} bytes", b.length);

    } finally {
      // remove it on failure
      if (deleteOnClose && !idxFile.delete())
        logger.error(" gc2 cant deleteOnClose index file {}", idxFile.getPath());
    }

    return true;
  }

  /*
  message Record {
    uint32 fileno = 1;               // which GRIB file ? key into GC.fileMap
    uint64 pos = 2;                  // offset in GRIB file of the start of entire message
    uint64 bmsPos = 3;               // use alternate bms if non-zero
    uint32 drsOffset = 4;            // offset of drs from pos (grib2 only)
  }

  // SparseArray only at the GCs (MRC and SRC) not at the Partitions
  // dont need SparseArray in memory until someone wants to read from the variable
  message SparseArray {
    repeated uint32 size = 2 [packed=true];     // multidim sizes = shape[]
    repeated uint32 track = 3 [packed=true];    // 1-based index into record list, 0 == missing
    repeated Record records = 4;                // List<Record>
    uint32 ndups = 5;                           // duplicates found when creating
  }
   */
  private GribCollectionProto.SparseArray writeSparseArray(Grib2CollectionBuilder.VariableBag vb, Set<Integer> fileSet) {
    GribCollectionProto.SparseArray.Builder b = GribCollectionProto.SparseArray.newBuilder();
    SparseArray<Grib2Record> sa = vb.coordND.getSparseArray();
    for (int size : sa.getShape())
      b.addSize(size);
    for (int track : sa.getTrack())
      b.addTrack(track);

    for (Grib2Record gr : sa.getContent()) {
      GribCollectionProto.Record.Builder br = GribCollectionProto.Record.newBuilder();

      br.setFileno(gr.getFile());
      fileSet.add(gr.getFile());
      long startPos = gr.getIs().getStartPos();
      br.setStartPos(startPos);

      if (gr.isBmsReplaced()) {
        Grib2SectionBitMap bms = gr.getBitmapSection();
        br.setBmsOffset((int) (bms.getStartingPosition() - startPos));
      }

      Grib2SectionDataRepresentation drs = gr.getDataRepresentationSection();
      br.setDrsOffset((int)(drs.getStartingPosition() - startPos));
      b.addRecords(br);
    }

    b.setNdups(sa.getNdups());
    return b.build();
  }

    /*
  message Dataset {
    required Type type = 1;
    repeated Group groups = 2;
   */
  private GribCollectionProto.Dataset writeDatasetProto(GribCollectionImmutable.Type type, List<Group> groups) {
    GribCollectionProto.Dataset.Builder b = GribCollectionProto.Dataset.newBuilder();

    GribCollectionProto.Dataset.Type ptype = GribCollectionProto.Dataset.Type.valueOf(type.toString());
    b.setType(ptype);

    for (Group group : groups)
      b.addGroups(writeGroupProto(group));

    return b.build();
  }

  /*
  message Group {
    Gds gds = 1;                             // use this to build the HorizCoordSys
    repeated Variable variables = 2;         // list of variables
    repeated Coord coords = 3;               // list of coordinates
    repeated uint32 fileno = 4 [packed=true]; // the component files that are in this group, key into gc.mfiles
  }
   */
  private GribCollectionProto.Group writeGroupProto(Group g) {
    GribCollectionProto.Group.Builder b = GribCollectionProto.Group.newBuilder();

    b.setGds( writeGdsProto(g.gdss.getRawBytes(), -1));

    for (Grib2CollectionBuilder.VariableBag vbag : g.gribVars) {
      b.addVariables(writeVariableProto(vbag));
    }

    for (Coordinate coord : g.coords) {
      switch (coord.getType()) {
        case runtime:
          b.addCoords(writeCoordProto((CoordinateRuntime) coord));
          break;
        case time:
          b.addCoords(writeCoordProto((CoordinateTime) coord));
          break;
        case timeIntv:
          b.addCoords(writeCoordProto((CoordinateTimeIntv) coord));
          break;
        case time2D:
          b.addCoords(writeCoordProto((CoordinateTime2D) coord));
          break;
        case vert:
          b.addCoords(writeCoordProto((CoordinateVert) coord));
          break;
        case ens:
          b.addCoords(writeCoordProto((CoordinateEns) coord));
          break;
      }
    }

    for (Integer aFileSet : g.fileSet)
      b.addFileno(aFileSet);

    return b.build();
  }

  /*
  message Variable {
   uint32 discipline = 1;
   bytes pds = 2;                   // raw pds
   repeated uint32 ids = 3 [packed=true];         // extra info not in pds; grib2 id section

   uint64 recordsPos = 4;  // offset of SparseArray message for this Variable
   uint32 recordsLen = 5;  // size of SparseArray message for this Variable

   repeated uint32 coordIdx = 6 [packed=true];    // indexes into Group.coords

   // optionally keep stats
   uint32 ndups = 8;
   uint32 nrecords = 9;
   uint32 missing = 10;

   // partition only
   repeated PartitionVariable partVariable = 100;
   }
   */
  private GribCollectionProto.Variable writeVariableProto(Grib2CollectionBuilder.VariableBag vb) {
    GribCollectionProto.Variable.Builder b = GribCollectionProto.Variable.newBuilder();

    b.setDiscipline(vb.first.getDiscipline());
    b.setPds(ByteString.copyFrom(vb.first.getPDSsection().getRawBytes()));

    // extra id info
    b.addIds(vb.first.getId().getCenter_id());
    b.addIds(vb.first.getId().getSubcenter_id());

    b.setRecordsPos(vb.pos);
    b.setRecordsLen(vb.length);

    for (int idx : vb.coordIndex)
      b.addCoordIdx(idx);

    // keep stats
    SparseArray sa = vb.coordND.getSparseArray();
    if (sa != null) {
      b.setNdups(sa.getNdups());
      b.setNrecords(sa.countNotMissing());
      b.setMissing(sa.countMissing());
    }

    return b.build();
  }

}


/*
 * Copyright (c) 1998 - 2014. University Corporation for Atmospheric Research/Unidata
 * Portions of this software were developed by the Unidata Program at the
 * University Corporation for Atmospheric Research.
 *
 * Access and use of this software shall impose the following obligations
 * and understandings on the user. The user is granted the right, without
 * any fee or cost, to use, copy, modify, alter, enhance and distribute
 * this software, and any derivative works thereof, and its supporting
 * documentation for any purpose whatsoever, provided that this entire
 * notice appears in all copies of the software, derivative works and
 * supporting documentation.  Further, UCAR requests that the user credit
 * UCAR/Unidata in any publications that result from the use of this
 * software or in any product that includes this software. The names UCAR
 * and/or Unidata, however, may not be used in any advertising or publicity
 * to endorse or promote any products or commercial entity unless specific
 * written permission is obtained from UCAR/Unidata. The user also
 * understands that UCAR/Unidata is not obligated to provide the user with
 * any support, consulting, training or assistance of any kind with regard
 * to the use, operation and performance of this software nor to provide
 * the user with any updates, revisions, new versions or "bug fixes."
 *
 * THIS SOFTWARE IS PROVIDED BY UCAR/UNIDATA "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 UCAR/UNIDATA BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE ACCESS, USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package thredds.server.catalog.builder;

import org.jdom2.Attribute;
import org.jdom2.DataConversionException;
import org.jdom2.Element;
import thredds.client.catalog.Catalog;
import ucar.nc2.util.AliasTranslator;
import thredds.server.catalog.DatasetScanConfig;
import ucar.unidata.util.StringUtil2;

import java.io.File;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Optional;

/**
 * Builder of DatasetScanConfig
 *
 * @author John
 * @since 1/12/2015
 */
public class DatasetScanConfigBuilder {
  Formatter errlog;
  boolean fatalError;

  public DatasetScanConfigBuilder(Formatter errlog) {
    this.errlog = errlog;
  }

  public boolean hasFatalError() {
    return fatalError;
  }

  /*
      <xsd:element name="datasetScan" substitutionGroup="dataset">
      <xsd:complexType>
        <xsd:complexContent>
          <xsd:extension base="DatasetType">
            <xsd:sequence>
              <xsd:element ref="filter" minOccurs="0" maxOccurs="1"/>
              <xsd:element ref="namer" minOccurs="0" maxOccurs="1"/>
              <xsd:element ref="sort" minOccurs="0" maxOccurs="1"/>
              <xsd:element ref="addLatest" minOccurs="0" maxOccurs="1"/>
              <xsd:element ref="addProxies" minOccurs="0" maxOccurs="1"/>
              <xsd:element name="addDatasetSize" minOccurs="0" maxOccurs="1"/>
              <xsd:element ref="addTimeCoverage" minOccurs="0" maxOccurs="1"/>
            </xsd:sequence>

            <xsd:attribute name="path" type="xsd:string" use="required"/>
            <xsd:attribute name="location" type="xsd:string"/>
            <xsd:attribute name="addLatest" type="xsd:boolean"/>
            <xsd:attribute name="filter" type="xsd:string"/> <!-- deprecated : use filter element -->
          </xsd:extension>
        </xsd:complexContent>
      </xsd:complexType>
    </xsd:element>
     */
  public DatasetScanConfig readDatasetScanConfig(Element dsElem) {
    DatasetScanConfig result = new DatasetScanConfig();

    result.name = dsElem.getAttributeValue("name");
    result.path = StringUtil2.trim(dsElem.getAttributeValue("path"), '/');
    if (result.path == null) {
      errlog.format("ERROR: must specify path attribute.%n");
      fatalError = true;
    }

    String scanDir = dsElem.getAttributeValue("location");
    if (scanDir == null) {
      errlog.format("ERROR: must specify directory root in location attribute.%n");
      fatalError = true;
    } else {
      result.scanDir = AliasTranslator.translateAlias(scanDir);
      File scanFile = new File(result.scanDir);
      if (!scanFile.exists()) {
        errlog.format("ERROR: directory %s does not exist%n", result.scanDir);
        fatalError = true;
      }
    }

    result.restrictAccess = dsElem.getAttributeValue("restrictAccess");

    // look for ncml
    Element ncmlElem = dsElem.getChild("netcdf", Catalog.defNS);
    if (ncmlElem != null) {
      ncmlElem.detach();
      result.ncmlElement = ncmlElem;
    }

    // Read filter element
    Element filterElem = dsElem.getChild("filter", Catalog.defNS);
    result.filters = readDatasetScanFilter(filterElem);

    // Read namer element
    Element namerElem = dsElem.getChild("namer", Catalog.defNS);
    result.namers = readDatasetScanNamer(namerElem);

    // Read filesSort or sort element
    Element filesSortElem = dsElem.getChild("filesSort", Catalog.defNS);
    if (filesSortElem != null)
      result.isSortIncreasing = readFilesSort(filesSortElem);
    Element sorterElem = dsElem.getChild("sort", Catalog.defNS);
    if (!result.isSortIncreasing.isPresent() && sorterElem != null)
      result.isSortIncreasing = readSort(sorterElem);

    // Deal with latest
    String addLatestAttribute = dsElem.getAttributeValue("addLatest");
    Element addLatestElem = dsElem.getChild("addLatest", Catalog.defNS);      // not in docs
    Element addProxiesElem = dsElem.getChild("addProxies", Catalog.defNS);
    result.addLatest = readDatasetScanAddProxies(addProxiesElem, addLatestElem, addLatestAttribute);

    /* Read addDatasetSize element.
    Element addDsSizeElem = dsElem.getChild("addDatasetSize", Catalog.defNS);
    if (addDsSizeElem != null) {                                               // docs: default true
      if (addDsSizeElem.getTextNormalize().equalsIgnoreCase("false"))
        result.addDatasetSize = false;
    } */

    // Read addTimeCoverage element.
    Element addTimeCovElem = dsElem.getChild("addTimeCoverage", Catalog.defNS);
    if (addTimeCovElem != null) {
      result.addTimeCoverage = readDatasetScanAddTimeCoverage(addTimeCovElem);
    }

    return result;
  }

  /*
    <xsd:element name="filter">
    <xsd:complexType>
      <xsd:choice>
        <xsd:sequence minOccurs="0" maxOccurs="unbounded">
          <xsd:element name="include" type="FilterSelectorType" minOccurs="0"/>
          <xsd:element name="exclude" type="FilterSelectorType" minOccurs="0"/>
        </xsd:sequence>
      </xsd:choice>
    </xsd:complexType>
  </xsd:element>

  <xsd:complexType name="FilterSelectorType">
    <xsd:attribute name="regExp" type="xsd:string"/>
    <xsd:attribute name="wildcard" type="xsd:string"/>
    <xsd:attribute name="atomic" type="xsd:boolean"/>
    <xsd:attribute name="collection" type="xsd:boolean"/>
  </xsd:complexType>
   */
  private List<DatasetScanConfig.Filter> readDatasetScanFilter(Element filterElem) {
    List<DatasetScanConfig.Filter> filters = new ArrayList<>();
    if (filterElem == null) return null;

    for (Element curElem : filterElem.getChildren()) {
      String regExpAttVal = curElem.getAttributeValue("regExp");
      String wildcardAttVal = curElem.getAttributeValue("wildcard");
      String lastModLimitAttValS = curElem.getAttributeValue("lastModLimitInMillis");
      if (regExpAttVal == null && wildcardAttVal == null && lastModLimitAttValS == null) {
        // If no regExp or wildcard attributes, skip this selector.
        errlog.format("WARN: readDatasetScanFilter(): no regExp, wildcard, or lastModLimitInMillis attribute in filter child <%s>%n", curElem.getName());

      } else {
        // Determine if applies to atomic datasets, default true.
        String atomicAttVal = curElem.getAttributeValue("atomic");
        boolean atomic = (atomicAttVal == null || !atomicAttVal.equalsIgnoreCase("false"));

        // Determine if applies to collection datasets, default false.
        String collectionAttVal = curElem.getAttributeValue("collection");
        boolean notCollection = collectionAttVal == null || !collectionAttVal.equalsIgnoreCase("true");

        // Determine if include or exclude selectors.
        boolean includer = true;
        if (curElem.getName().equals("exclude")) {
          includer = false;
        } else if (!curElem.getName().equals("include")) {
          errlog.format("WARN: readDatasetScanFilter(): unhandled filter child <%s>.%n", curElem.getName());
          continue;
        }

        // check for errors
        long lastModLimitAttVal = -1;
        if (lastModLimitAttValS != null) {
          try {
            lastModLimitAttVal = Long.parseLong(lastModLimitAttValS);
          } catch (NumberFormatException e) {
            errlog.format("WARN: readDatasetScanFilter(): lastModLimitInMillis not valid <%s>.%n", curElem);
          }
        }

        filters.add(new DatasetScanConfig.Filter(regExpAttVal, wildcardAttVal, lastModLimitAttVal, atomic, !notCollection, includer));
      }
    }

    return filters;
  }

  /*
  <xsd:element name="namer">
    <xsd:complexType>
      <xsd:choice maxOccurs="unbounded">
        <xsd:element name="regExpOnName" type="NamerSelectorType"/>
        <xsd:element name="regExpOnPath" type="NamerSelectorType"/>
      </xsd:choice>
    </xsd:complexType>
  </xsd:element>

  <xsd:complexType name="NamerSelectorType">
    <xsd:attribute name="regExp" type="xsd:string"/>
    <xsd:attribute name="replaceString" type="xsd:string"/>
  </xsd:complexType>
   */

  protected List<DatasetScanConfig.Namer> readDatasetScanNamer(Element namerElem) {
    List<DatasetScanConfig.Namer> result = new ArrayList<>();
    if (namerElem == null) return result;

    for (Element curElem : namerElem.getChildren()) {
      String regExp = curElem.getAttributeValue("regExp");
      String replaceString = curElem.getAttributeValue("replaceString");

      boolean onName = curElem.getName().equals("regExpOnName");
      boolean onPath = curElem.getName().equals("regExpOnPath");
      if (!onName && !onPath) {
        errlog.format("WARN: readDatasetScanNamer(): namer child '%s'%n", curElem.getName());
        continue;
      }
      result.add(new DatasetScanConfig.Namer(onName, regExp, replaceString));
    }
    return result;
  }

  /*
  <xsd:element name="sort">
    <xsd:complexType>
    <xsd:choice>
     <xsd:element name="lexigraphicByName">
       <xsd:complexType>
         <xsd:attribute name="increasing" type="xsd:boolean"/>
       </xsd:complexType>
     </xsd:element>
     <xsd:element name="crawlableDatasetSorterImpl" minOccurs="0" type="UserImplType"/>
    </xsd:choice>
    </xsd:complexType>
  </xsd:element>
   */

  protected Optional<Boolean> readFilesSort(Element sorterElem) {
    String increasingString = sorterElem.getAttributeValue("increasing");
    if (increasingString != null) {
      if (increasingString.equalsIgnoreCase("true"))
        return Optional.of(true);
      else if (increasingString.equalsIgnoreCase("false"))
        return Optional.of(false);
    }
    return Optional.empty();
  }

  protected Optional<Boolean> readSort(Element sorterElem) {
    Element lexSortElem = sorterElem.getChild("lexigraphicByName", Catalog.defNS);
    if (lexSortElem != null) {
      String increasingString = lexSortElem.getAttributeValue("increasing");
      if (increasingString != null) {
        if (increasingString.equalsIgnoreCase("true"))
          return Optional.of(true);
        else if (increasingString.equalsIgnoreCase("false"))
          return Optional.of(false);
      }
    }
    return Optional.empty();
  }

  protected DatasetScanConfig.AddLatest readDatasetScanAddProxies(Element addProxiesElem, Element addLatestElem, String addLatestAttribute) {

    // handle "addLatest attribute
    if (addLatestAttribute != null && addLatestAttribute.equalsIgnoreCase("true")) {
      return new DatasetScanConfig.AddLatest();  // use defaults
    }

    // Handle "addLatest" elements.
    if (addLatestElem != null) {
      return readDatasetScanAddLatest(addLatestElem);
    }

    // Handle old "addProxies" elements.
    if (addProxiesElem != null) {
      for (Element curChildElem : addProxiesElem.getChildren()) {

        // Handle "simpleLatest" child elements.
        if (curChildElem.getName().equals("simpleLatest")) {
          return readDatasetScanAddLatest(curChildElem);
        }

        if (curChildElem.getName().equals("latestComplete")) {
          return readDatasetScanAddLatest(curChildElem);
        }
      }
    }

    return null;
  }

  /*
   <xsd:complexType name="addLatestType">
    <xsd:attribute name="name" type="xsd:string"/>
    <xsd:attribute name="top" type="xsd:boolean"/>
    <xsd:attribute name="serviceName" type="xsd:string"/>
    <xsd:attribute name="lastModifiedLimit" type="xsd:float"/> <!-- minutes -->
  </xsd:complexType>
   */

  private DatasetScanConfig.AddLatest readDatasetScanAddLatest(Element addLatestElem) {

    String latestName = "latest.xml";
    String serviceName = "Resolver";
    boolean latestOnTop = true;
    boolean isResolver = true;

    String tmpLatestName = addLatestElem.getAttributeValue("name");
    if (tmpLatestName != null)
      latestName = tmpLatestName;

    String tmpserviceName = addLatestElem.getAttributeValue("serviceName");
     if (tmpserviceName != null)
       serviceName = tmpserviceName;

     // Does latest go on top or bottom of list.
    Attribute topAtt = addLatestElem.getAttribute("top");
    if (topAtt != null) {
      try {
        latestOnTop = topAtt.getBooleanValue();
      } catch (DataConversionException e) {
        latestOnTop = true;
      }
    }

    // Get lastModifed limit.
    String lastModLimitVal = addLatestElem.getAttributeValue("lastModifiedLimit");
    long lastModLimit = -1;
    if (lastModLimitVal != null)
      lastModLimit = Long.parseLong(lastModLimitVal) * 60 * 1000; // convert minutes to millisecs

    return new DatasetScanConfig.AddLatest(latestName, serviceName, latestOnTop, lastModLimit);
  }

/*
  <xsd:element name="addTimeCoverage">
    <xsd:complexType>
      <xsd:attribute name="datasetNameMatchPattern" type="xsd:string"/>
      <xsd:attribute name="datasetPathMatchPattern" type="xsd:string"/>
      <xsd:attribute name="startTimeSubstitutionPattern" type="xsd:string"/>
      <xsd:attribute name="duration" type="xsd:string"/>
    </xsd:complexType>
  </xsd:element>
 */

  protected DatasetScanConfig.AddTimeCoverage readDatasetScanAddTimeCoverage(Element addTimeCovElem) {
    String matchName = addTimeCovElem.getAttributeValue("datasetNameMatchPattern");
    String matchPath = addTimeCovElem.getAttributeValue("datasetPathMatchPattern");
    String subst = addTimeCovElem.getAttributeValue("startTimeSubstitutionPattern");
    String duration = addTimeCovElem.getAttributeValue("duration");

    boolean err = false;
    if (subst == null) {
      errlog.format("WARN: readDatasetScanAddTimeCoverage(): must have startTimeSubstitutionPattern elem=<%s>%n", addTimeCovElem);
      err = true;
    } else if (duration == null) {
      errlog.format("WARN: readDatasetScanAddTimeCoverage(): must have duration elem=<%s>%n", addTimeCovElem);
      err = true;
    } else if (matchName == null && matchPath == null) {
      errlog.format("WARN: readDatasetScanAddTimeCoverage(): must have either datasetNameMatchPattern or datasetPathMatchPattern elem=<%s>%n", addTimeCovElem);
      err = true;
    }

    return err ? null : new DatasetScanConfig.AddTimeCoverage(matchName, matchPath, subst, duration);
  }

}


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

package ucar.nc2.ft.remote;

import ucar.httpservices.HTTPFactory;
import ucar.httpservices.HTTPMethod;
import ucar.httpservices.HTTPSession;
import ucar.nc2.ft.*;
import ucar.nc2.ft.point.remote.PointDatasetRemote;
import ucar.nc2.ft.point.writer.FeatureDatasetCapabilitiesWriter;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.ft2.coverage.FeatureDatasetCoverage;
import ucar.nc2.ft2.coverage.remote.CdmrfReader;
import ucar.nc2.stream.CdmRemote;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.util.Optional;
import ucar.unidata.geoloc.LatLonRect;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.output.XMLOutputter;
import org.jdom2.input.SAXBuilder;

/**
 * Factory for FeatureDataset using cdmrFeature protocol.
 * This object represents the client, connecting to a remote dataset.
 * This handles both coverages (now in ucar.nc2.ft2) and point (ucar.nc2.ft) feature types.
 *
 * @author caron
 * @since May 19, 2009
 */
public class CdmrFeatureDataset {
  static public final String PROTOCOL = "cdmrFeature";
  static public final String SCHEME = PROTOCOL + ":";

  static private boolean debug = false;
  static private boolean showXML = false;

  // all CdmrFeatureDatasets must return their featureType - use as a fail-fast test of the endpoint
  public static FeatureType isCdmrfEndpoint(String endpoint) throws IOException {

    HTTPSession httpClient = HTTPFactory.newSession(endpoint);
    String url = endpoint + "?req=featureType";

    // get the header
    try (HTTPMethod method = HTTPFactory.Get(httpClient, url)) {
      method.setFollowRedirects(true);
      int statusCode = method.execute();
      if (statusCode != 200) return null;
      String content = method.getResponseAsString();
      return FeatureType.getType(content);

    } catch (Throwable t) {
      t.printStackTrace();
      return null;
    }
  }

  static public Optional<FeatureDataset> factory(FeatureType wantFeatureType, String endpoint) throws IOException {
    if (endpoint.startsWith(SCHEME))
      endpoint = endpoint.substring(SCHEME.length());

    FeatureType featureType;
    try {
      featureType = isCdmrfEndpoint(endpoint);
      if (featureType == null) return Optional.empty("Not a valid CdmrFeatureDataset endpoint="+endpoint);

    } catch (IOException ioe) {
      return Optional.empty(String.format("Error opening CdmrFeatureDataset endpoint=%s err=%s", endpoint, ioe.getMessage()));
    }

    if (!FeatureDatasetFactoryManager.featureTypeOk(wantFeatureType, featureType))
      return Optional.empty(String.format("Not a compatible featureType=%s, want=%s, endpoint=%s", featureType, wantFeatureType, endpoint));

    if (featureType.isCoverageFeatureType()) {
      CdmrfReader reader = new CdmrfReader(endpoint);
      CoverageCollection covColl = reader.open();
      return Optional.of( new FeatureDatasetCoverage(endpoint, covColl, covColl));
    }

    if (featureType.isPointFeatureType()) {
      Document doc = getCapabilities(endpoint);
      Element root = doc.getRootElement();
      Element elem = root.getChild("featureDataset");
      String fType = elem.getAttribute("type").getValue();  // LOOK, may be multiple types

      endpoint = elem.getAttribute("url").getValue();
      wantFeatureType = FeatureType.getType(fType);
      if (debug) System.out.printf("CdmrFeatureDataset endpoint %s%n ftype= '%s' url=%s%n", endpoint, fType, endpoint);

      List<VariableSimpleIF> dataVars = FeatureDatasetCapabilitiesWriter.getDataVariables(doc);
      LatLonRect bb = FeatureDatasetCapabilitiesWriter.getSpatialExtent(doc);
      CalendarDateRange dr = FeatureDatasetCapabilitiesWriter.getTimeSpan(doc);
      CalendarDateUnit timeUnit = FeatureDatasetCapabilitiesWriter.getTimeUnit(doc);
      String altUnits = FeatureDatasetCapabilitiesWriter.getAltUnits(doc);

      return Optional.of(new PointDatasetRemote(wantFeatureType, endpoint, timeUnit, altUnits, dataVars, bb, dr));
    }

    return Optional.empty(String.format("Unimplemented featureType=%s, want=%s, endpoint=%s", featureType, wantFeatureType, endpoint));
  }

  static private org.jdom2.Document getCapabilities(String endpoint) throws IOException {
    org.jdom2.Document doc;
    InputStream in = null;
    try {
      in = CdmRemote.sendQuery(null, endpoint, "req=capabilities");
      SAXBuilder builder = new SAXBuilder();
      doc = builder.build(in);  // LOOK closes in when done ??

    } catch (Throwable t) {
      throw new IOException(t);

    } finally {
      //if (in != null)
      //  in.close();
    }

    if (showXML) {
      System.out.printf("*** endpoint = %s %n", endpoint);
      XMLOutputter xmlOut = new XMLOutputter();
      System.out.printf("*** CdmrFeatureDataset/showParsedXML = %n %s %n", xmlOut.outputString(doc));
    }

    return doc;
  }

}


/////////////////////////////////////////////////////////////////////////////
// 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;

/**
 * OptSwitch	- class definition for Switches
 *

 * Description:  this class defines a switch element.
 * a switch is considered to be used (having been set or assigned
 * a value if the 'set' field is false AND the 'val' field is null.
 *

 * Constants:
 *

 * Permitted values for 'type' field:
 *

 * protected static final int NONE 	uninitialized
 * protected static final int BOOL 	boolean type switch
 * protected static final int VAL  	value type switch
 *

 * Fields:	sw	the switch name
 * type	boolean/value
 * set	value is set/clear
 * val	switch value (not applicable to boolean type switch)
 *
 * @author Arieh Markel
 * @version 1.0
 */
public class OptSwitch extends Object {
    protected static final int NONE = 0;
    protected static final int BOOL = 1;
    protected static final int VAL = 2;

    int sw;        // switch name
    int type;        // boolean/value
    public boolean set;    // switch is set/unset
    public String val;        // value of switch
    boolean debug = false;

    public OptSwitch() {
        set = false;
        type = NONE;
        val = null;
        sw = -1;
    }

    /**
     * Invocation with explicit Character switchname and type
     *
     * @param c
     * @param type
     */
    public OptSwitch(Character c, int type) {
        sw = Character.digit(c.charValue(), 10);
        this.type = type;
        set = false;
        val = null;
        if (debug) {
            System.out.println("sw = " + (char) sw + "; type = " + type +
                    "; set = " + set + "; val = " + val);
        }
    }

    /**
     * Invocation with explicit integer switchname and type
     *
     * @param c
     * @param type
     */
    public OptSwitch(int c, int type) {
        sw = c;
        this.type = type;
        set = false;
        val = null;
        if (debug) {
            System.out.println("sw = " + (char) sw + "; type = " + type +
                    "; set = " + set + "; val = " + val);
        }
    }

    /**
     * Set the value type of the option switch to the type passed
     *
     * @param    type    type of value that switch may accept or be
     */
    public void SetHasValue(int type) {
        this.type = type;
        if (debug) {
            System.out.println("sw = " + (char) sw + "; type = " + type +
                    "; set = " + set + "; val = " + val);
        }
    }

    /**
     * Return whether the option switch accepts values or no
     */
    public boolean acceptVal() {
        return type == VAL;
    }

    /**
     * Set the 'set' field of the option switch to 'b'.
     *
     * @param b set the 'set' boolean field to 'b'.
     */
    public void SetVal(boolean b) {
        set = b;
    }

    /**
     * Set the 'val' field of the option switch to 's'.
     *
     * @param s string to assign to 'val' field.
     */
    public void SetVal(String s) {
        val = s;
    }
}


package ucar.nc2.ogc.gml;

import net.opengis.gml.x32.TimePeriodType;
import ucar.nc2.ft.StationTimeSeriesFeature;
import ucar.nc2.ft.point.CollectionInfo;
import ucar.nc2.ft.point.DsgCollectionHelper;
import ucar.nc2.ogc.MarshallingUtil;
import ucar.nc2.time.CalendarDateRange;

import java.io.IOException;

/**
 * Created by cwardgar on 3/6/14.
 */
public abstract class NcTimePeriodType {
    // wml2:Collection/wml2:observationMember/om:OM_Observation/om:phenomenonTime/gml:TimePeriod
    public static TimePeriodType initTimePeriod(
            TimePeriodType timePeriod, StationTimeSeriesFeature stationFeat) throws IOException {
        // @gml:id
        String id = MarshallingUtil.createIdForType(TimePeriodType.class);
        timePeriod.setId(id);

        CollectionInfo info;
        try {
            info = new DsgCollectionHelper(stationFeat).calcBounds();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        CalendarDateRange cdr = info.getCalendarDateRange(stationFeat.getTimeUnit());
        if (cdr != null) {

            // gml:beginPosition
            NcTimePositionType.initBeginPosition(timePeriod.addNewBeginPosition(), cdr.getStart());

            // gml:endPosition
            NcTimePositionType.initEndPosition(timePeriod.addNewEndPosition(), cdr.getEnd());
        }

        return timePeriod;
    }

    private NcTimePeriodType() { }
}


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

package ucar.nc2.grib.grib1;

/**
 * Helper class for pre-defined grid definition section (GDS) .
 * These are NCEP.
 *
 * @see "http://www.nco.ncep.noaa.gov/pmb/docs/on388/tableb.html"
 */

public class Grib1GdsPredefined {

  /**
   * Constructs a Grib1Gds object from a pds and predefined tables.
   *
   * @param center center id
   * @param gridNumber from pds (octet 7)
   * @return predefined GDS
   */
  public static Grib1Gds factory(int center, int gridNumber) {
    if (center == 7) {
      return factoryNCEP(gridNumber);
    } else
      throw new IllegalArgumentException("Dont have predefined GDS " + gridNumber + " from " + center);
  }

  // 21-26, 61-64: International Exchange and Family of Services (FOS) grids. So may be more general than NCEP

  private static Grib1Gds factoryNCEP(int gridNumber) {
    switch (gridNumber) {
      case 21:
        return new NcepLatLon(gridNumber, 37, 36, 0.0F, 0.0F, 90.0F, 180.0F, 5.0F, 2.5F, (byte) 0x88, (byte) 64);
      case 22:
        return new NcepLatLon(gridNumber, 37, 36, 0.0F, -180.0F, 90.0F, 0.0F, 5.0F, 2.5F, (byte) 0x88, (byte) 64);
      case 23:
        return new NcepLatLon(gridNumber, 37, 36, -90.0F, 0.0F, 180.0F, 0.0F, 5.0F, 2.5F, (byte) 0x88, (byte) 64);
      case 24:
        return new NcepLatLon(gridNumber, 37, 36, -90.0F, -180.0F, 0.0F, 0.0F, 5.0F, 2.5F, (byte) 0x88, (byte) 64);
      case 25:
        return new NcepLatLon(gridNumber, 72, 18, 0.0F, 0.0F, 90.0F, 355.0F, 5.0F, 5.0F, (byte) 0x88, (byte) 64);
      case 26:
        return new NcepLatLon(gridNumber, 72, 18, -90.0F, 0.0F, 0.0F, 355.0F, 5.0F, 5.0F, (byte) 0x88, (byte) 64);
      case 61:
        return new NcepLatLon(gridNumber, 91, 45, 0.0F, 0.0F, 90.0F, 180.0F, 2.0F, 2.0F, (byte) 0x88, (byte) 64);
      case 62:
        return new NcepLatLon(gridNumber, 91, 45, -90.0F, 0.0F, 0.0F, 180.0F, 2.0F, 2.0F, (byte) 0x88, (byte) 64);
      case 63:
        return new NcepLatLon(gridNumber, 91, 45, -90.0F, 0.0F, 0.0F, 180.0F, 2.0F, 2.0F, (byte) 0x88, (byte) 64);
      case 64:
        return new NcepLatLon(gridNumber, 91, 45, -90.0F, -180.0F, 0.0F, 0.0F, 2.0F, 2.0F, (byte) 0x88, (byte) 64);
      case 87:
        return new NcepPS(gridNumber, 81, 62, 22.8756F, 239.5089F, 255.0F, 68153.0F, 68153.0F, (byte) 0x08, (byte) 64);
    }
    throw new IllegalArgumentException("Dont have predefined GDS " + gridNumber + " from NCEP (center 7)");
  }

  private static class NcepLatLon extends Grib1Gds.LatLon {
    final int gridNumber;

    NcepLatLon(int gridNumber, int nx, int ny, float la1, float lo1, float la2, float lo2, float deltaLon, float deltaLat,
               byte resolution, byte scan) {
      super(1000 * gridNumber);
      this.gridNumber = gridNumber;
      this.nx = nx;
      this.ny = ny;
      this.la1 = la1;
      this.lo1 = lo1;
      this.la2 = la2;
      this.lo2 = lo2;
      this.deltaLon = deltaLon;
      this.deltaLat = deltaLat;
      this.resolution = resolution;
      this.scanMode = scan;
    }

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

      NcepLatLon that = (NcepLatLon) o;
      if (gridNumber != that.gridNumber) return false;
      return true;
    }

    @Override
    public int hashCode() {
      return gridNumber;
    }
  }

  private static class NcepPS extends Grib1Gds.PolarStereographic {
    final int gridNumber;

    NcepPS(int gridNumber, int nx, int ny, float la1, float lo1, float lov, float dX, float dY,
           byte resolution, byte scan) {
      super(1000 * gridNumber);
      this.gridNumber = gridNumber;
      this.nx = nx;
      this.ny = ny;
      this.la1 = la1;
      this.lo1 = lo1;
      this.lov = lov;
      this.dX = dX;
      this.dY = dY;
      this.resolution = resolution;
      this.scanMode = scan;
    }

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

      NcepPS that = (NcepPS) o;
      if (gridNumber != that.gridNumber) return false;
      return true;
    }

    @Override
    public int hashCode() {
      return gridNumber;
    }
  }

}