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

package ucar.nc2.iosp;

import ucar.unidata.io.RandomAccessFile;

import java.io.EOFException;
import java.io.IOException;

/**
 * Helper for reading data that has been bit packed.
 *
 * @author caron
 * @since Apr 7, 2008
 */
public class BitReader {

  private static final int BIT_LENGTH = Byte.SIZE;
  private static final int BYTE_BITMASK = 0xFF;
  private static final long LONG_BITMASK = Long.MAX_VALUE;

  private RandomAccessFile raf = null;
  private long startPos;

  private byte[] data;
  private int dataPos;

  private byte bitBuf = 0;
  private int bitPos = 0; // Current bit position in bitBuf.

  // for testing
  public BitReader(byte[] test) {
    this.data = test;
    this.dataPos = 0;
  }

  /**
   * Constructor
   *
   * @param raf      the RandomAccessFile
   * @param startPos points to start of data in data section, in bytes
   * @throws IOException on read error
   */
  public BitReader(RandomAccessFile raf, long startPos) throws IOException {
    this.raf = raf;
    this.startPos = startPos;
    raf.seek(startPos);
  }

  /**
   * Go to the next byte in the stream
   */
  public void incrByte() {
    this.bitPos = 0;
  }

  /**
   * Position file at bitOffset from startPos
   *
   * @param bitOffset bit offset from starting position
   * @throws IOException on io error
   */
  public void setBitOffset(int bitOffset) throws IOException {
    if (bitOffset % 8 == 0) {
      raf.seek(startPos + bitOffset / 8);
      bitPos = 0;
      bitBuf = 0;
    } else {
      raf.seek(startPos + bitOffset / 8);
      bitPos = 8 - (bitOffset % 8);
      bitBuf = (byte) raf.read();
      bitBuf &= 0xff >> (8 - bitPos);   // mask off consumed bits
    }
  }

  public long getPos() throws IOException {
    if (raf != null) {
      return raf.getFilePointer();
    } else {
      return dataPos;
    }
  }

  /**
   * Read the next nb bits and return an Unsigned Long .
   *
   * @param nb the number of bits to convert to int, must be 0 <= nb <= 64.
   * @return result
   * @throws java.io.IOException on read error
   */
  public long bits2UInt(int nb) throws IOException {
    assert nb <= 64;
    assert nb >= 0;

    long result = 0;
    int bitsLeft = nb;

    while (bitsLeft > 0) {

      // we ran out of bits - fetch the next byte...
      if (bitPos == 0) {
        bitBuf = nextByte();
        bitPos = BIT_LENGTH;
      }

      // -- retrieve bit from current byte ----------
      // how many bits to read from the current byte
      int size = Math.min(bitsLeft, bitPos);
      // move my part to start
      int myBits = bitBuf >> (bitPos - size);
      // mask-off sign-extending
      myBits &= BYTE_BITMASK;
      // mask-off bits of next value
      myBits &= ~(BYTE_BITMASK << size);

      // -- put bit to result ----------------------
      // where to place myBits inside of result
      int shift = bitsLeft - size;
      assert shift >= 0;

      // put it there
      result |= myBits << shift;

      // -- put bit to result ----------------------
      // update information on what we consumed
      bitsLeft -= size;
      bitPos -= size;
    }

    return result;
  }

  /**
   * Read the next nb bits and return an Signed Long .
   *
   * @param nb the number of bits to convert to int, must be <= 64.
   * @return result
   * @throws java.io.IOException on read error
   */
  public long bits2SInt(int nb) throws IOException {

    long result = bits2UInt(nb);

    // check if we're negative
    if (getBit(result, nb)) {
      // it's negative! reset leading bit
      result = setBit(result, nb, false);
      // build 2's-complement
      result = ~result & LONG_BITMASK;
      result = result + 1;
    }

    return result;

  }

  private byte nextByte() throws IOException {
    if (raf != null) {
      int result = raf.read();
      if (result == -1)
        throw new EOFException();
      return (byte) result;
    } else {
      return data[dataPos++];
    }
  }

  public static long setBit(long decimal, int N, boolean value) {
    return value ? decimal | (1 << (N - 1)) : decimal & ~(1 << (N - 1));
  }

  public static boolean getBit(long decimal, int N) {
    int constant = 1 << (N - 1);
    return (decimal & constant) > 0;
  }

}


/////////////////////////////////////////////////////////////////////////////
// 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 opendap.dap.parsers.ParseException;
import org.apache.http.Header;
import org.apache.http.HttpStatus;
import org.apache.http.auth.InvalidCredentialsException;
import ucar.httpservices.HTTPException;
import ucar.httpservices.HTTPFactory;
import ucar.httpservices.HTTPMethod;
import ucar.httpservices.HTTPSession;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

/**
 * Rewritten 1/15/07 jcaron to use HttpCLient library instead of jdk UrlConnection class.
 * Need more robust redirect and authentication.
 *

 * This class provides support for common DODS client-side operations such as
 * dereferencing a OPeNDAP URL, communicating network activity status
 * to the user and reading local OPeNDAP objects.
 *

 * Unlike its C++ counterpart, this class does not store instances of the DAS,
 * DDS, etc. objects. Rather, the methods getDAS, etc. return
 * instances of those objects.
 *
 * @author jehamby
 */
public class DConnect2 implements Closeable
{

    static private boolean allowSessions = false;
    static private boolean showCompress = false;

    static final Charset UTF8 = Charset.forName("UTF-8");

    static public synchronized void setAllowSessions(boolean b)
    {
        allowSessions = b;
    }

    private String urlString; // The current DODS URL without Constraint Expression
    private String filePath = null; // if url is file://
    private InputStream stream = null; //if reading from a stream

    /**
     * The projection portion of the current DODS CE (including leading "?").
     */
    private String projString;

    /**
     * The selection portion of the current DODS CE (including leading "&").
     */
    private String selString;

    /**
     * Whether to accept compressed documents.
     */
    private boolean acceptCompress;

    // various stuff that comes from the HTTP headers
    private String lastModified = null;
    private String lastExtended = null;
    private String lastModifiedInvalid = null;
    private boolean hasSession = true;
    protected HTTPSession _session = null;

    private ServerVersion ver; // The OPeNDAP server version.

    private boolean debugHeaders = false, debugStream = false;

    public void setServerVersion(int major, int minor)
    {
        //LogStream.out.println("ServerVersion made with int,int: " + major + "," + minor);

        ver = new ServerVersion(major, minor);
        //LogStream.out.println("ServerVersion.getMajor(): " + ver.getMajor());
        //LogStream.out.println("ServerVersion.getMinor(): " + ver.getMinor());
    }

    /**
     * Creates an instance bound to url which accepts compressed documents.
     *
     * @param urlString connect to this URL.
     * @throws FileNotFoundException thrown if urlString is not
     *                               a valid URL, or a filename which exists on the system.
     */
    public DConnect2(String urlString) throws HTTPException
    {
        this(urlString, true);
    }

    /**
     * Creates an instance bound to url. If acceptDeflate is true
     * then HTTP Request headers will indicate to servers that this client can
     * accept compressed documents.
     *
     * @param urlString      Connect to this URL.
     * @param acceptCompress true if this client will accept compressed responses
     * @throws FileNotFoundException thrown if urlString is not
     *                               a valid URL, or a filename which exists on the system.
     */
    public DConnect2(String urlString, boolean acceptCompress)
            throws HTTPException
    {
        int ceIndex = urlString.indexOf('?');
        if(ceIndex >= 0) {
            this.urlString = urlString.substring(0, ceIndex);
            String expr = urlString.substring(ceIndex + 1);
            int selIndex = expr.indexOf('&');
            if(selIndex >= 0) {
                this.projString = expr.substring(0, selIndex);
                this.selString = expr.substring(selIndex);
            } else {
                this.projString = expr;
                this.selString = "";
            }
        } else {
            this.urlString = urlString;
            this.projString = this.selString = "";
        }
        this.acceptCompress = acceptCompress;
        // Check out the URL to see if it is file://
        try {
            URL testURL = new URL(urlString);
            if("file".equals(testURL.getProtocol())) {
                filePath = testURL.getPath();
                // See if .dds and .dods files exist
                File f = new File(filePath + ".dds");
                if(!f.canRead()) {
                    throw new HTTPException("file not readable: " + urlString + ".dds");
                }
                f = new File(filePath + ".dods");
                if(!f.canRead()) {
                    throw new HTTPException("file not readable: " + urlString + ".dods");
                }
            } else {
                _session = HTTPFactory.newSession(this.urlString);
            }
    /* Set the server version cause we won't get it from anywhere */
            ver = new ServerVersion(ServerVersion.DAP2_PROTOCOL_VERSION, ServerVersion.XDAP);
        } catch (DAP2Exception ex) {
            throw new HTTPException("Cannot set server version");
        } catch (MalformedURLException e) {
            throw new HTTPException("Malformed URL: " + urlString);
        }
    }

/*
 * Creates an instance bound to an Inputstream.
 * @param stream   to get data from
 * @throws IOException  thrown if stream read fails.
 */

    public DConnect2(InputStream stream) throws DAP2Exception
    {
        this.stream = stream;
  /* Set the server version cause we won't get it from anywhere */
        ver = new ServerVersion(ServerVersion.DAP2_PROTOCOL_VERSION, ServerVersion.XDAP);
    }

/*
 *
 * @return  if reading from file:// or stream
 */

    public boolean isLocal()
    {
        return (stream != null || filePath != null);
    }

    /**
     * Returns the constraint expression supplied with the URL given to the
     * constructor. If no CE was given this returns an empty String.
     *

     * Note that the CE supplied to one of this object's constructors is
     * "sticky"; it will be used with every data request made with this object.
     * The CE passed to getData, however, is not sticky; it is used
     * only for that specific request. This method returns the sticky CE.
     *
     * @return the constraint expression associated with this connection.
     */
    public final String CE()
    {
        return projString + selString;
    }

    /**
     * Returns the URL supplied to the constructor. If the URL contained a
     * constraint expression that is not returned.
     *
     * @return the URL of this connection.
     */
    public final String URL()
    {
        return urlString;
    }

    /**
     * Return the session associated with this connection
     *
     * @return this connections session (or null)
     */
    public HTTPSession getSession()
    {
        return _session;
    }

    /**
     * Open a connection to the DODS server.
     *
     * @param urlString the URL to open; assume already properly encoded
     * @param command   execute this command on the input stream
     * @throws IOException   if an IO exception occurred.
     * @throws DAP2Exception if the DODS server returned an error.
     */
    private void openConnection(String urlString, Command command) throws IOException, DAP2Exception
    {
        InputStream is = null;

        try {
            try (HTTPMethod method = HTTPFactory.Get(_session, urlString)) {

                if(acceptCompress)
                    method.setCompression("deflate,gzip");

                // enable sessions
                if(allowSessions)
                    method.setUseSessions(true);
                int statusCode;
                for(; ; ) {
                    statusCode = method.execute();
                    if(statusCode != HttpStatus.SC_SERVICE_UNAVAILABLE)
                        break;
                    Thread.sleep(5000);
                    System.err.println("Service Unavailable");
                }

                // debug
                // if (debugHeaders) ucar.httpservices.HttpClientManager.showHttpRequestInfo(f, method);

                if(statusCode == HttpStatus.SC_NOT_FOUND) {
                    throw new DAP2Exception(DAP2Exception.NO_SUCH_FILE, method.getStatusText() + ": " + urlString);
                }

                if(statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
                    throw new InvalidCredentialsException(method.getStatusText());
                }

                if(statusCode != HttpStatus.SC_OK) {
                    throw new DAP2Exception("Method failed:" + method.getStatusText() + " on URL= " + urlString);
                }

                // Get the response body.
                is = method.getResponseAsStream();

                // check if its an error
                Header header = method.getResponseHeader("Content-Description");
                if(header != null && (header.getValue().equals("dods-error")
                        || header.getValue().equals("dods_error"))) {
                    // create server exception object
                    DAP2Exception ds = new DAP2Exception();
                    // parse the Error object from stream and throw it
                    ds.parse(is);
                    throw ds;
                }

                ver = new ServerVersion(method);

                checkHeaders(method);

                // check for deflator
                Header h = method.getResponseHeader("content-encoding");
                String encoding = (h == null) ? null : h.getValue();
                //if (encoding != null) LogStream.out.println("encoding= " + encoding);

                if(encoding != null && encoding.equals("deflate")) {
                    is = new BufferedInputStream(new InflaterInputStream(is), 1000);
                    if(showCompress) System.out.printf("deflate %s%n", urlString);

                } else if(encoding != null && encoding.equals("gzip")) {
                    is = new BufferedInputStream(new GZIPInputStream(is), 1000);
                    if(showCompress) System.out.printf("gzip %s%n", urlString);
                } else {
                    if(showCompress) System.out.printf("none %s%n", urlString);
                }

                command.process(is);
            }

        } catch (IOException | DAP2Exception e) {
            throw e;

        } catch (Exception e) {
            Util.check(e);
            //e.printStackTrace();
            throw new DAP2Exception(e);
        }
    }

    public void close()
    {
        try {
            if(allowSessions && hasSession) {
                openConnection(urlString + ".close", new Command()
                {
                    public void process(InputStream is) throws IOException
                    {
                        captureStream(is);
                    }
                });
            }
            if(_session != null)
                _session.close();
        } catch (Throwable t) {
            // ignore
        }
    }

    static public String captureStream(InputStream is)
            throws IOException
    {
        ByteArrayOutputStream text = new ByteArrayOutputStream();
        int b;
        while((b = is.read()) >= 0) {
            text.write(b);
        }
        return new String(text.toByteArray(), UTF8);
    }

// Extract the leading DDS from a DataDDS stream

    static final byte[] tag1 = "\nData:\n".getBytes(UTF8);
    static final byte[] tag2 = "\nData:\r\n".getBytes(UTF8);

    static public String captureDataDDS(InputStream is)
            throws IOException
    {
        byte[] text = new byte[4096];
        int pos = 0;
        int len = text.length;
        int b;
        boolean eof = true;
        while((b = is.read()) >= 0) {
            text = need(1, pos, len, text);
            len = text.length;
            text[pos++] = (byte) b;
            if(b == '\n') { // write until \nData:\n || \nData:\r\n
                // look for datatags
                if(endswith(tag1, pos, text)) {
                    pos -= tag1.length; // dont return tag
                    eof = false;
                    break;
                } else if(endswith(tag2, pos, text)) {
                    pos -= tag2.length; // dont return tag
                    eof = false;
                    break;
                }
            }
        }
        if(eof)
            throw new IOException("DatDDS has no 'Data:' marker");
        return new String(text, 0, pos, UTF8);
    }

    static private boolean endswith(byte[] tag, int pos, byte[] text)
    {
        int i, j;
        int taglen = tag.length;

        if(pos < taglen) return false;

        for(j = 0, i = pos - taglen; i < pos; i++, j++) {
            if(text[i] != tag[j]) return false;
        }
        return true;
    }

    static private byte[] need(int n, int pos, int len, byte[] text)
    {
        if(len - pos >= n) return text;
        int newlen = len * 2 + n;
        while(newlen < n) {
            newlen++;
        }
        byte[] newtext = new byte[newlen];
        System.arraycopy(text, 0, newtext, 0, len);
        return newtext;
    }

    /**
     * Returns the ServerVersion of the last connection.
     *
     * @return the ServerVersion of the last connection.
     */
    public final ServerVersion getServerVersion()
    {
        return ver;
    }

    /**
     * @return value of Last-Modified Header from last connection, may be null
     */
    public String getLastModifiedHeader()
    {
        return lastModified;
    }

    /**
     * @return value of X-Last-Modified-Invalid Header from last connection, may be null
     */
    public String getLastModifiedInvalidHeader()
    {
        return lastModifiedInvalid;
    }

    /**
     * @return value of Last-Extended Header from last connection, may be null
     */
    public String getLastExtendedHeader()
    {
        return lastExtended;
    }

    private void checkHeaders(HTTPMethod method)
    {
        if(debugHeaders) {
            DAPNode.log.debug("\nOpenConnection Headers for " + method.getPath());
            DAPNode.log.debug("Status Line: " + method.getStatusLine());
        }

        Header[] responseHeaders = method.getResponseHeaders();
        for(int i1 = 0; i1 < responseHeaders.length; i1++) {
            Header responseHeader = responseHeaders[i1];
            if(debugHeaders) DAPNode.log.debug("  " + responseHeader);
            String key = responseHeader.getName();
            String value = responseHeader.getValue();

            if(key.equals("Last-Modified")) {
                lastModified = value;
                if(debugHeaders)
                    DAPNode.log.debug(" **found lastModified = " + lastModified);

            } else if(key.equals("X-Last-Extended")) {
                lastExtended = value;
                if(debugHeaders)
                    DAPNode.log.debug(" **found lastExtended = " + lastExtended);

            } else if(key.equals("X-Last-Modified-Invalid")) {
                lastModifiedInvalid = value;
                if(debugHeaders)
                    DAPNode.log.debug(" **found lastModifiedInvalid = " + lastModifiedInvalid);
            }
        }

        if(debugHeaders)
            DAPNode.log.debug("OpenConnection Headers for " + method.getPath());

    }

    private interface Command
    {
        void process(InputStream is) throws DAP2Exception, IOException;
    }

    /**
     * Returns the DAS object from the dataset referenced by this object's URL.
     * The DAS object is referred to by appending `.das' to the end of a DODS
     * URL.
     *
     * @return the DAS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the
     *                               constructor has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws DASException          on an error constructing the DAS
     * @throws DAP2Exception         if an error returned by the remote server
     */
    public DAS getDAS() throws IOException, DAP2Exception
    {
        DASCommand command = new DASCommand();
        if(filePath != null) { // url was file:
            File daspath = new File(filePath + ".das");
            // See if the das file exists
            if(daspath.canRead()) {
                try (FileInputStream is = new FileInputStream(daspath)) {
                    command.process(is);
                }
            }
        } else if(stream != null) {
            command.process(stream);
        } else { // assume url is remote
            try {
                openConnection(urlString + ".das" + getCompleteCE(projString,selString), command);
            } catch (DAP2Exception de) {
                //if(de.getErrorCode() != DAP2Exception.NO_SUCH_FILE)
                //throw de;  // rethrow
            }
        }
        return command.das;
    }

    static private class DASCommand implements Command
    {
        DAS das = new DAS();

        public void process(InputStream is) throws DAP2Exception, ParseException
        {
            das.parse(is);
        }
    }

    /**
     * Returns the DDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.dds' to the end of a OPeNDAP
     * URL.
     *
     * @return the DDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     */
    public DDS getDDS() throws IOException, ParseException, DAP2Exception
    {
        return getDDS("");
    }

    /**
     * Returns the DDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.dds' to the end of a OPeNDAP
     * URL.
     *
     * @param CE The constraint expression to be applied to this request by the
     *           server.  This is combined with any CE given in the constructor.
     * @return the DDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     */
    public DDS getDDS(String CE) throws IOException, ParseException, DAP2Exception
    {
        DDSCommand command = new DDSCommand();
        command.setURL(CE == null || CE.length() == 0 ? urlString : urlString + "?" + CE);
        if(filePath != null) {
            try (FileInputStream is = new FileInputStream(filePath + ".dds")) {
                command.process(is);
            }
        } else if(stream != null) {
            command.process(stream);
        } else { // must be a remote url
            openConnection(urlString + ".dds" + (getCompleteCE(CE)), command);
        }
        return command.dds;
    }

    static private class DDSCommand implements Command
    {
        DDS dds = new DDS();
        String url = null;

        public void setURL(String url)
        {
            this.url = url;
            if(dds != null && url != null) dds.setURL(url);
        }

        public void process(InputStream is) throws ParseException, DAP2Exception
        {
            dds.parse(is);
        }
    }

    /**
     * Use some sense when assembling the CE. Since this DConnect
     * object may have constructed using a CE, any new CE will
     * have to be integrated into it for subsequent requests.
     * Try to do this in a sensible manner!
     *
     * @param CE The new CE from the client.
     * @return The complete CE (the one this object was built
     * with integrated with the clients)
     */
    private String getCompleteCE(String CE)
    {
        String localProjString = null;
        String localSelString = null;
        if(CE == null)
            return "";
        //remove any leading '?'
        if(CE.startsWith("?")) CE = CE.substring(1);
        int selIndex = CE.indexOf('&');
        if(selIndex == 0) {
            localProjString = "";
            localSelString = CE;
        } else if(selIndex > 0) {
            localSelString = CE.substring(selIndex);
            localProjString = CE.substring(0, selIndex);
        } else {// selIndex < 0
            localProjString = CE;
            localSelString = "";
        }

        String ce = projString;

        if(!localProjString.equals("")) {
            if(!ce.equals("") && localProjString.indexOf(',') != 0)
                ce += ",";
            ce += localProjString;
        }

        if(!selString.equals("")) {
            if(selString.indexOf('&') != 0)
                ce += "&";
            ce += selString;
        }

        if(!localSelString.equals("")) {
            if(localSelString.indexOf('&') != 0)
                ce += "&";
            ce += localSelString;
        }

        if(ce.length() > 0) ce = "?" + ce;

        if(false) {
            DAPNode.log.debug("projString: '" + projString + "'");
            DAPNode.log.debug("localProjString: '" + localProjString + "'");
            DAPNode.log.debug("selString: '" + selString + "'");
            DAPNode.log.debug("localSelString: '" + localSelString + "'");
            DAPNode.log.debug("Complete CE: " + ce);
        }
        return ce;   // escaping will happen elsewhere
    }

    /**
     * ALternate interface to getCompleteCE(String ce)
     *
     * @param proj the projection
     * @param sel  the selection
     * @return the combined constraint
     */
    private String
    getCompleteCE(String proj, String sel)
    {
        if(proj != null && proj.length() == 0) proj = "";  // canonical
        if(sel != null && sel.length() == 0) sel = null;  // canonical
        StringBuilder buf = new StringBuilder();
        if(proj.startsWith("?"))
            buf.append(proj.substring(1));
        else
            buf.append(proj);
        if(sel != null) {
            if(sel.startsWith("&"))
                buf.append(sel);
            else {
                buf.append("&");
                buf.append(sel);
            }
        }
        return getCompleteCE(buf.toString());
    }

    /**
     * Returns the DDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.ddx' to the end of a OPeNDAP
     * URL. The server should send back a DDX (A DDS in XML format) which
     * will get parsed here (locally) and a new DDS instantiated using
     * the DDSXMLParser.
     *
     * @return the DDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     * @opendap.ddx.experimental
     */
    public DDS getDDX() throws IOException, ParseException, DAP2Exception
    {
        return (getDDX(""));
    }

    /**
     * Returns the DDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.ddx' to the end of a OPeNDAP
     * URL. The server should send back a DDX (A DDS in XML format) which
     * will get parsed here (locally) and a new DDS instantiated using
     * the DDSXMLParser.
     *
     * @param CE The constraint expression to be applied to this request by the
     *           server.  This is combined with any CE given in the constructor.
     * @return the DDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     * @opendap.ddx.experimental
     */
    public DDS getDDX(String CE) throws IOException, ParseException, DDSException, DAP2Exception
    {
        DDXCommand command = new DDXCommand();
        openConnection(urlString + ".ddx" + (getCompleteCE(CE)), command);
        return command.dds;
    }

    static private class DDXCommand implements Command
    {
        DDS dds = new DDS();

        public void process(InputStream is) throws DAP2Exception, ParseException
        {
            dds.parseXML(is, false);
        }
    }

    /**
     * Returns the DataDDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.ddx' to the end of a OPeNDAP
     * URL. The server should send back a DDX (A DDS in XML format) which
     * will get parsed here (locally) and a new DDS instantiated using
     * the DDSXMLParser.
     *
     * @return the DataDDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     * @opendap.ddx.experimental
     */
    public DataDDS getDataDDX() throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {

        return getDataDDX("", new DefaultFactory());
    }

    /**
     * Returns the DataDDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.ddx' to the end of a OPeNDAP
     * URL. The server should send back a DDX (A DDS in XML format) which
     * will get parsed here (locally) and a new DDS instantiated using
     * the DDSXMLParser.
     *
     * @param CE The constraint expression to use for this request.
     * @return the DataDDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     * @opendap.ddx.experimental
     */
    public DataDDS getDataDDX(String CE) throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {

        return getDataDDX(CE, new DefaultFactory());
    }

    /**
     * Returns the DataDDS object from the dataset referenced by this object's URL.
     * The DDS object is referred to by appending `.ddx' to the end of a OPeNDAP
     * URL. The server should send back a DDX (A DDS in XML format) which
     * will get parsed here (locally) and a new DDS instantiated using
     * the DDSXMLParser.
     *
     * @param CE  The constraint expression to use for this request.
     * @param btf The BaseTypeFactory to build the member
     *            variables in the DDS with.
     * @return the DataDDS associated with the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if an error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if an error returned by the remote server
     * @opendap.ddx.experimental
     * @see BaseTypeFactory
     */
    public DataDDS getDataDDX(String CE, BaseTypeFactory btf) throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {

        DataDDXCommand command = new DataDDXCommand(btf, this.ver);
        openConnection(urlString + ".ddx" + (getCompleteCE(CE)), command);
        return command.dds;
    }

    static private class DataDDXCommand implements Command
    {
        DataDDS dds;

        DataDDXCommand(BaseTypeFactory btf, ServerVersion ver)
        {
            dds = new DataDDS(ver, btf);
        }

        public void process(InputStream is) throws DAP2Exception, ParseException
        {
            dds.parseXML(is, false);
        }
    }

    /**
     * Returns the `Data object' from the dataset referenced by this object's
     * URL given the constraint expression CE. Note that the Data object is
     * really just a DDS object with data bound to the variables. The DDS will
     * probably contain fewer variables (and those might have different
     * types) than in the DDS returned by getDDS() because that method returns
     * the entire DDS (but without any data) while this method returns
     * only those variables listed in the projection part of the constraint
     * expression.
     *

     * Note that if CE is an empty String then the entire dataset will be
     * returned, unless a "sticky" CE has been specified in the constructor.
     *
     * @param CE       The constraint expression to be applied to this request by the
     *                 server.  This is combined with any CE given in the constructor.
     * @param statusUI the StatusUI object to use for GUI updates
     *                 and user cancellation notification (may be null).
     * @param btf      The BaseTypeFactory to build the member
     *                 variables in the DDS with.
     * @return The DataDDS object that results from applying the
     * given CE, combined with this object's sticky CE, on the referenced
     * dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if any error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if any error returned by the remote server
     */
    public DataDDS getData(String CE, StatusUI statusUI, BaseTypeFactory btf) throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {
        if(CE != null && CE.trim().length() == 0) CE = null;
        DataDDS dds = new DataDDS(ver, btf);
        DataDDSCommand command = new DataDDSCommand(dds, statusUI);
        command.setURL(urlString + (CE == null ? "" : "?" + CE));
        if(filePath != null) { // url is file:
            File dodspath = new File(filePath + ".dods");
            // See if the dods file exists
            if(dodspath.canRead()) {
      /* WARNING: any constraints are ignored in reading the file */
                try (FileInputStream is = new FileInputStream(dodspath)) {
                    command.process(is);
                }
            }
        } else if(stream != null) {
            command.process(stream);
        } else {
            String urls = urlString + ".dods" + (CE == null ? "" : getCompleteCE(CE));
            openConnection(urls, command);
        }
        return command.dds;
    }

    static private class DataDDSCommand implements Command
    {
        DataDDS dds = null;
        StatusUI statusUI;
        //Coverity[FB.URF_UNREAD_FIELD]
        String url = null;

        DataDDSCommand(DataDDS dds, StatusUI statusUI)
        {
            this.dds = dds;
            this.statusUI = statusUI;
        }

        public void setURL(String url)
        {
            this.url = url;
        }

        ;

        public void process(InputStream is) throws ParseException, DAP2Exception, IOException
        {
            if(!dds.parse(is))
                throw new DAP2Exception("DataDDS DDS parse failed");
            dds.readData(is, statusUI);
        }
    }

    public DataDDS getData(String CE) throws IOException, ParseException, DAP2Exception
    {
        return getData(CE, null, new DefaultFactory());
    }

/**
 * Returns the `Data object' from the dataset referenced by this object's
 * URL given the constraint expression CE. Note that the Data object is
 * really just a DDS object with data bound to the variables. The DDS will
 * probably contain fewer variables (and those might have different
 * types) than in the DDS returned by getDDS() because that method returns
 * the entire DDS (but without any data) while this method returns
 * only those variables listed in the projection part of the constraint
 * expression.
 *

 * Note that if CE is an empty String then the entire dataset will be
 * returned, unless a "sticky" CE has been specified in the constructor.
 *

 *

 * This method uses the 2 step method for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.
 *
 * @param CE       The constraint expression to be applied to this request by the
 *                 server.  This is combined with any CE given in the constructor.
 * @param statusUI the StatusUI object to use for GUI updates
 *                 and user cancellation notification (may be null).
 * @return The DataDDS object that results from applying the
 *         given CE, combined with this object's sticky CE, on the referenced
 *         dataset.
 * @throws MalformedURLException if the URL given to the constructor
 *                               has an error
 * @throws IOException           if any error connecting to the remote server
 * @throws ParseException        if the DDS parser returned an error
 * @throws DDSException          on an error constructing the DDS
 * @throws DAP2Exception         if any error returned by the remote server
 * @opendap.ddx.experimental
 *
public DataDDS getDDXData(String CE, StatusUI statusUI, BaseTypeFactory btf) throws MalformedURLException, IOException,
ParseException, DDSException, DAP2Exception {

String urls = urlString + ".ddx" + getCompleteCE(CE);

DataDDS dds = new DataDDS(ver, btf);
DataDDXCommand command = new DataDDXCommand(dds, statusUI);
openConnection(urls, command);

return command.dds;
}

private class DataDDXCommand implements Command {
DataDDS dds;
StatusUI statusUI;

DataDDXCommand(DataDDS dds, StatusUI statusUI) {
this.dds = dds;
this.statusUI = statusUI;
}

/*
 * Returns the `Data object' from the dataset referenced by this object's
 * URL given the constraint expression CE. Note that the Data object is
 * really just a DDS object with data bound to the variables. The DDS will
 * probably contain fewer variables (and those might have different
 * types) than in the DDS returned by getDDS() because that method returns
 * the entire DDS (but without any data) while this method returns
 * only those variables listed in the projection part of the constraint
 * expression.
 *

 * Note that if CE is an empty String then the entire dataset will be
 * returned, unless a "sticky" CE has been specified in the constructor.
 *

 *

 * This method uses the 2 step method for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.

public void process(InputStream is) throws DAP2Exception, ParseException, IOException {
dds.parseXML(is, false);    // read the DDX
getBlobData(dds, statusUI);
}
}  */

/**
 * Opens the BLOB uRL in the DDS supplied and deserializes that binary content
 * sent from the server cooresponding to the DDS.
 *

 *

 * This method is the 2nd step in the 2 step process for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.
 *
 * @param dds      The DDS containing the BLOB URL and into which the BLOB
 *                 (serialized binary content) will be deserialized.
 * @param statusUI the StatusUI object to use for GUI updates
 *                 and user cancellation notification (may be null).
 * @throws MalformedURLException if the URL given to the constructor
 *                               has an error
 * @throws IOException           if any error connecting to the remote server
 * @throws ParseException        if the DDS parser returned an error
 * @throws DDSException          on an error constructing the DDS
 * @throws DAP2Exception         if any error returned by the remote server
 *
private void getBlobData(DataDDS dds, StatusUI statusUI)
throws MalformedURLException, IOException,
ParseException, DDSException, DAP2Exception {

/* boolean dumpStreamErr = false; // opendap.util.util.Debug.isSet("dumpStreamErr");

DAPNode.debug("dds.getBlobURL(): " + dds.getBlobContentID());
LogStream.dbg.logflush();

if (dds.getBlobContentID() == null) {
throw new MalformedURLException("Blob URL was 'null'. " +
"This may indicate that this OPeNDAP Server does not support the full use of DDX.");
}

URL blobURL = new URL(dds.getBlobContentID());

DAPNode.debug("Opening BLOB URL: " + blobURL);
LogStream.dbg.logflush();

InputStream is = openConnection(blobURL);

try {

dds.readData(is, statusUI); // read the data!

} catch (Throwable e) {
LogStream.err.println("DConnect dds.readData problem with: " + blobURL + "\nStack Trace:");
e.printStackTrace(LogStream.err);
LogStream.err.logflush();

throw new DAP2Exception("Connection problem when reading: " + blobURL + "\n" +
"Error Message - " + e.toString());

} finally {
is.close();  // stream is always closed even if parse() throws exception
if (connection instanceof HttpURLConnection)
((HttpURLConnection) connection).disconnect();
}

} */

/**
 * Returns the `Data object' from the dataset referenced by this object's
 * URL given the constraint expression CE. Note that the Data object is
 * really just a DDS object with data bound to the variables. The DDS will
 * probably contain fewer variables (and those might have different
 * types) than in the DDS returned by getDDS() because that method returns
 * the entire DDS (but without any data) while this method returns
 * only those variables listed in the projection part of the constraint
 * expression.
 *

 * Note that if CE is an empty String then the entire dataset will be
 * returned, unless a "sticky" CE has been specified in the constructor.
 *

 *

 * This method uses the 2 step method for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.
 *
 * @param url      The complete URL of the dataset. Constraint Expression included.
 * @param statusUI the StatusUI object to use for GUI updates
 *                 and user cancellation notification (may be null).
 * @param btf      The BaseTypeFactory to build the member
 *                 variables in the DDS with.
 * @return The DataDDS object that results from applying the
 *         given CE, combined with this object's sticky CE, on the referenced
 *         dataset.
 * @throws MalformedURLException if the URL given to the constructor
 *                               has an error
 * @throws IOException           if any error connecting to the remote server
 * @throws ParseException        if the DDS parser returned an error
 * @throws DDSException          on an error constructing the DDS
 * @throws DAP2Exception         if any error returned by the remote server
 * @opendap.ddx.experimental
 *
public DataDDS getDDXDataFromURL(URL url, StatusUI statusUI, BaseTypeFactory btf)
throws IOException,
ParseException, DDSException, DAP2Exception {

DAPNode.debug("Opening DDX URL: " + url);
LogStream.dbg.logflush();

InputStream is = openConnection(url);
DataDDS dds = new DataDDS(ver, btf);

boolean dumpStreamErr = false; // opendap.util.util.Debug.isSet("dumpStreamErr");

try {

dds.parseXML(is, false);    // read the DDX

//dds.parse(new HeaderInputStream(is));    // read the DDS header
// NOTE: the HeaderInputStream will have skipped over "Data:" line

} catch (Throwable e) {
LogStream.err.println("DConnect ddx.parse problem with: " + url + "\nStack Trace:");
e.printStackTrace(LogStream.err);
LogStream.err.logflush();

throw new DAP2Exception("Connection problem when reading: " + url + "\n" +
"Error Message - " + e.toString());

} finally {
is.close();  // stream is always closed even if parse() throws exception
if (connection instanceof HttpURLConnection)
((HttpURLConnection) connection).disconnect();
}

getBlobData(dds, statusUI);

return dds;
} */

    /**
     * Returns the `Data object' from the dataset referenced by this object's
     * URL given the constraint expression CE. Note that the Data object is
     * really just a DDS object with data bound to the variables. The DDS will
     * probably contain fewer variables (and those might have different
     * types) than in the DDS returned by getDDS() because that method returns
     * the entire DDS (but without any data) while this method returns
     * only those variables listed in the projection part of the constraint
     * expression.
     *

     * Note that if CE is an empty String then the entire dataset will be
     * returned, unless a "sticky" CE has been specified in the constructor.
     *
     * @param CE       The constraint expression to be applied to this request by the
     *                 server.  This is combined with any CE given in the constructor.
     * @param statusUI the StatusUI object to use for GUI updates
     *                 and user cancellation notification (may be null).
     * @return The DataDDS object that results from applying the
     * given CE, combined with this object's sticky CE, on the referenced
     * dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if any error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if any error returned by the remote server
     */
    public DataDDS getData(String CE, StatusUI statusUI) throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {

        return getData(CE, statusUI, new DefaultFactory());
    }

/**
 * Returns the `Data object' from the dataset referenced by this object's
 * URL given the constraint expression CE. Note that the Data object is
 * really just a DDS object with data bound to the variables. The DDS will
 * probably contain fewer variables (and those might have different
 * types) than in the DDS returned by getDDS() because that method returns
 * the entire DDS (but without any data) while this method returns
 * only those variables listed in the projection part of the constraint
 * expression.
 *

 * Note that if CE is an empty String then the entire dataset will be
 * returned, unless a "sticky" CE has been specified in the constructor.
 *

 *

 * This method uses the 2 step method for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.
 *
 * @param CE       The constraint expression to be applied to this request by the
 *                 server.  This is combined with any CE given in the constructor.
 * @param statusUI the StatusUI object to use for GUI updates
 *                 and user cancellation notification (may be null).
 * @return The DataDDS object that results from applying the
 *         given CE, combined with this object's sticky CE, on the referenced
 *         dataset.
 * @throws MalformedURLException if the URL given to the constructor
 *                               has an error
 * @throws IOException           if any error connecting to the remote server
 * @throws ParseException        if the DDS parser returned an error
 * @throws DDSException          on an error constructing the DDS
 * @throws DAP2Exception         if any error returned by the remote server
 * @opendap.ddx.experimental
 *
public DataDDS getDDXData(String CE, StatusUI statusUI) throws MalformedURLException, IOException,
ParseException, DDSException, DAP2Exception {

return getDDXData(CE, statusUI, new DefaultFactory());
} */

    /**
     * Return the data object with no local constraint expression.  Same as
     * getData("", statusUI).
     *
     * @param statusUI the StatusUI object to use for GUI updates
     *                 and user cancellation notification (may be null).
     * @return The DataDDS object that results from applying
     * this object's sticky CE, if any, on the referenced dataset.
     * @throws MalformedURLException if the URL given to the constructor
     *                               has an error
     * @throws IOException           if any error connecting to the remote server
     * @throws ParseException        if the DDS parser returned an error
     * @throws DDSException          on an error constructing the DDS
     * @throws DAP2Exception         if any error returned by the remote server
     */
    public final DataDDS getData(StatusUI statusUI) throws MalformedURLException, IOException,
            ParseException, DDSException, DAP2Exception
    {
        return getData("", statusUI, new DefaultFactory());
    }

/**
 * Returns the `Data object' from the dataset referenced by this object's
 * URL given the constraint expression CE. Note that the Data object is
 * really just a DDS object with data bound to the variables. The DDS will
 * probably contain fewer variables (and those might have different
 * types) than in the DDS returned by getDDS() because that method returns
 * the entire DDS (but without any data) while this method returns
 * only those variables listed in the projection part of the constraint
 * expression.
 *

 * Note that if CE is an empty String then the entire dataset will be
 * returned, unless a "sticky" CE has been specified in the constructor.
 *

 *

 * This method uses the 2 step method for aquiring data from a server using
 * a DDX and a BLOB. First, a DDX (an XML representation of a DDS) is requested.
 * The DDX is parsed and a DataDDS is created.
 * The DDX contains a URL that points to the servers BLOB service. The BLOB
 * service returns only the serialized binary content of the DataDDS. The DataDDS
 * then deserializes the BLOB and fills itself with data.
 *
 * @param statusUI the StatusUI object to use for GUI updates
 *                 and user cancellation notification (may be null).
 * @return The DataDDS object that results from applying the
 *         given CE, combined with this object's sticky CE, on the referenced
 *         dataset.
 * @throws MalformedURLException if the URL given to the constructor
 *                               has an error
 * @throws IOException           if any error connecting to the remote server
 * @throws ParseException        if the DDS parser returned an error
 * @throws DDSException          on an error constructing the DDS
 * @throws DAP2Exception         if any error returned by the remote server
 * @opendap.ddx.experimental
 *
public final DataDDS getDDXData(StatusUI statusUI) throws MalformedURLException, IOException,
ParseException, DDSException, DAP2Exception {
return getDDXData("", statusUI, new DefaultFactory());
} */

    /**
     * @param args command line arguments
     */
    public static void main(String[] args)
    {
/*
  DConnect2 dc;

  for (String url : args) {
    try {
      LogStream.out.println("");
      LogStream.out.println("");
      LogStream.out.println("########################################################");
      LogStream.out.println("\nConnecting to " + url + "\n");
      dc = new DConnect2(url);

      LogStream.out.println("\n- - - - - - - - - - - - - - - - - - -");

      LogStream.out.println("Retrieving DDS:\n");
      DDS dds = dc.getDDS();
      dds.print(LogStream.out);

      LogStream.out.println("\n- - - - - - - - - - - - - - - - - - -");
      LogStream.out.println("Retrieving DAS:\n");
      DAS das = dc.getDAS();
      das.print(LogStream.out);

      LogStream.out.println("\n- - - - - - - - - - - - - - - - - - -");
      LogStream.out.println("Retrieving DATA:\n");
      dds = dc.getData("");
      dds.printVal(LogStream.out, "");

      LogStream.out.println("\n- - - - - - - - - - - - - - - - - - -");
      LogStream.out.println("Retrieving DDX:\n");
      dds = dc.getDDX();
      dds.printXML(LogStream.out);

      LogStream.out.logflush();
    }
    catch (Throwable t) {
      t.printStackTrace(LogStream.err);
    }     */

    }

}


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

package ucar.unidata.geoloc.projection;

import com.google.common.math.DoubleMath;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.util.Misc;
import ucar.unidata.geoloc.*;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import static ucar.unidata.geoloc.LatLonPointImmutable.INVALID;

/**
 * Sinusoidal projection, spherical earth.
 * See John Snyder, Map Projections used by the USGS, Bulletin 1532, 2nd edition (1983), p 243
 *
 * @author John Caron
 * @since Feb 24, 2013
 */

public class Sinusoidal extends ProjectionImpl {
    private final double earthRadius;
    private double centMeridian; // central Meridian in degrees
    private double falseEasting, falseNorthing;

    @Override
    public ProjectionImpl constructCopy() {
        ProjectionImpl result = new Sinusoidal(getCentMeridian(), getFalseEasting(), getFalseNorthing(),
                getEarthRadius());
        result.setDefaultMapArea(defaultMapArea);
        result.setName(name);
        return result;
    }

    /**
     * Constructor with default parameters
     */
    public Sinusoidal() {
        this(0.0, 0.0, 0.0, EARTH_RADIUS);
    }

    /**
     * Construct a Sinusoidal Projection.
     *
     * @param centMeridian   central Meridian (degrees)
     * @param false_easting  false_easting in km
     * @param false_northing false_northing in km
     * @param radius         earth radius in km
     */
    public Sinusoidal(double centMeridian, double false_easting, double false_northing, double radius) {
        super(CF.SINUSOIDAL, false);

        this.centMeridian = centMeridian;
        this.falseEasting = false_easting;
        this.falseNorthing = false_northing;
        this.earthRadius = radius;

        addParameter(CF.GRID_MAPPING_NAME, CF.SINUSOIDAL);
        addParameter(CF.LONGITUDE_OF_CENTRAL_MERIDIAN, centMeridian);
        addParameter(CF.EARTH_RADIUS, earthRadius * 1000);

        if ((false_easting != 0.0) || (false_northing != 0.0)) {
            addParameter(CF.FALSE_EASTING, false_easting);
            addParameter(CF.FALSE_NORTHING, false_northing);
            addParameter(CDM.UNITS, "km");
        }

    }

    /**
     * Get the central Meridian in degrees
     *
     * @return the central Meridian
     */
    public double getCentMeridian() {
        return centMeridian;
    }

    /**
     * Get the false easting, in km.
     *
     * @return the false easting.
     */
    public double getFalseEasting() {
        return falseEasting;
    }

    /**
     * Get the false northing, in km.
     *
     * @return the false northing.
     */
    public double getFalseNorthing() {
        return falseNorthing;
    }

    public double getEarthRadius() {
        return earthRadius;
    }

    //////////////////////////////////////////////
    // setters for IDV serialization - do not use except for object creating

    /**
     * Set the central Meridian
     *
     * @param centMeridian central Meridian in degrees
     */
    public void setCentMeridian(double centMeridian) {
        this.centMeridian = centMeridian;
    }

    /**
     * Set the false_easting, in km.
     * natural_x_coordinate + false_easting = x coordinate
     *
     * @param falseEasting x offset
     */
    public void setFalseEasting(double falseEasting) {
        this.falseEasting = falseEasting;
    }

    /**
     * Set the false northing, in km.
     * natural_y_coordinate + false_northing = y coordinate
     *
     * @param falseNorthing y offset
     */
    public void setFalseNorthing(double falseNorthing) {
        this.falseNorthing = falseNorthing;
    }

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

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

        Sinusoidal that = (Sinusoidal) o;

        if (Double.compare(that.centMeridian, centMeridian) != 0) {
            return false;
        }
        if (Double.compare(that.earthRadius, earthRadius) != 0) {
            return false;
        }
        if (Double.compare(that.falseEasting, falseEasting) != 0) {
            return false;
        }
        if (Double.compare(that.falseNorthing, falseNorthing) != 0) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        temp = earthRadius != +0.0d ? Double.doubleToLongBits(earthRadius) : 0L;
        result = (int) (temp ^ (temp >>> 32));
        temp = centMeridian != +0.0d ? Double.doubleToLongBits(centMeridian) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = falseEasting != +0.0d ? Double.doubleToLongBits(falseEasting) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = falseNorthing != +0.0d ? Double.doubleToLongBits(falseNorthing) : 0L;
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("Sinusoidal");
        sb.append("{earthRadius=").append(earthRadius);
        sb.append(", centMeridian=").append(centMeridian);
        sb.append(", falseEasting=").append(falseEasting);
        sb.append(", falseNorthing=").append(falseNorthing);
        sb.append('}');
        return sb.toString();
    }

    /**
     * Get the parameters as a String
     *
     * @return the parameters as a String
     */
    @Override
    public String paramsToString() {
        return toString();
    }

    /**
     * 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
     */
    @Override
    public boolean crossSeam(ProjectionPoint pt1, ProjectionPoint pt2) {
        // either point is infinite
        if (ProjectionPointImpl.isInfinite(pt1) || ProjectionPointImpl.isInfinite(pt2)) {
            return true;
        }

        // opposite signed long lines
        double x1 = pt1.getX() - falseEasting;
        double x2 = pt2.getX() - falseEasting;
        return (x1 * x2 < 0) && (Math.abs(x1 - x2) > earthRadius);
    }

    /**
     * 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
     */
    @Override
    public ProjectionPoint latLonToProj(LatLonPoint latLon, ProjectionPointImpl result) {
        double deltaLon_d = LatLonPointImpl.range180(latLon.getLongitude() - centMeridian);
        double fromLat_r = Math.toRadians(latLon.getLatitude());

        double toX = earthRadius * Math.toRadians(deltaLon_d) * Math.cos(fromLat_r);
        double toY = earthRadius * fromLat_r; // p 247 Snyder

        result.setLocation(toX + falseEasting, toY + falseNorthing);
        return result;
    }

    /**
     * Convert projection coordinates to a LatLonPoint
     *
     * @param world  convert from these projection coordinates
     * @param result the object to write to
     * @return LatLonPoint the lat/lon coordinates
     */
    @Override
    public LatLonPoint projToLatLon(ProjectionPoint world, LatLonPointImpl result) {
        double fromX = world.getX() - falseEasting;
        double fromY = world.getY() - falseNorthing;

        double toLat_r = fromY / earthRadius;
        double toLon_r;

        if (Misc.nearlyEquals(Math.abs(toLat_r), PI_OVER_2, 1e-10)) {
            toLat_r = toLat_r < 0 ? -PI_OVER_2 : +PI_OVER_2;
            toLon_r = Math.toRadians(centMeridian);  // if lat == +- pi/2, set lon = centMeridian (Snyder 248)
        } else if (Math.abs(toLat_r) < PI_OVER_2) {
            toLon_r = Math.toRadians(centMeridian) + fromX / (earthRadius * Math.cos(toLat_r));
        } else {
            return INVALID;  // Projection point is off the map.
        }

        if (Misc.nearlyEquals(Math.abs(toLon_r), PI, 1e-10)) {
            toLon_r = toLon_r < 0 ? -PI : +PI;
        } else if (Math.abs(toLon_r) > PI) {
            return INVALID;  // Projection point is off the map.
        }

        result.setLatitude(Math.toDegrees(toLat_r));
        result.setLongitude(Math.toDegrees(toLon_r));
        return result;
    }

    @Override
    public LatLonRect projToLatLonBB(ProjectionRect projBB) {
        List<ProjectionPoint> pointsOfInterest = new LinkedList<>();

        ProjectionPoint northPole = latLonToProj(new LatLonPointImpl(90, 0));
        if (projBB.contains(northPole)) {
            pointsOfInterest.add(northPole);
        }

        ProjectionPoint southPole = latLonToProj(new LatLonPointImpl(-90, 0));
        if (projBB.contains(southPole)) {
            pointsOfInterest.add(southPole);
        }

        if (pointsOfInterest.size() == 2) {  // projBB contains both north and south poles, and thus, the entire map.
            return new LatLonRect(new LatLonPointImpl(-90, -180), new LatLonPointImpl(90, 180));
        }

        List<ProjectionPoint> corners = Arrays.asList(projBB.getLowerLeftPoint(), projBB.getLowerRightPoint(),
                projBB.getUpperLeftPoint(), projBB.getUpperRightPoint());

        for (ProjectionPoint corner : corners) {
            if (projToLatLon(corner) != INVALID) {
                pointsOfInterest.add(corner);
            }
        }

        pointsOfInterest.addAll(getMapEdgeIntercepts(projBB));

        return makeLatLonRect(pointsOfInterest);
    }

    /**
     * Returns the points at which {@code projBB} intersects the map edge.
     *
     * @param projBB  defines a bounding box that may intersect the map edge, in projection coordinates.
     * @return        the points at which {@code projBB} intersects the map edge. May be empty.
     */
    public List<ProjectionPoint> getMapEdgeIntercepts(ProjectionRect projBB) {
        List<ProjectionPoint> intercepts = new LinkedList<>();

        for (ProjectionPoint topIntercept : getMapEdgeInterceptsAtY(projBB.getUpperRightPoint().getY())) {
            if (pointIsBetween(topIntercept, projBB.getUpperLeftPoint(), projBB.getUpperRightPoint())) {
                intercepts.add(topIntercept);
            }
        }

        for (ProjectionPoint rightIntercept : getMapEdgeInterceptsAtX(projBB.getUpperRightPoint().getX())) {
            if (pointIsBetween(rightIntercept, projBB.getUpperRightPoint(), projBB.getLowerRightPoint())) {
                intercepts.add(rightIntercept);
            }
        }

        for (ProjectionPoint bottomIntercept : getMapEdgeInterceptsAtY(projBB.getLowerLeftPoint().getY())) {
            if (pointIsBetween(bottomIntercept, projBB.getLowerLeftPoint(), projBB.getLowerRightPoint())) {
                intercepts.add(bottomIntercept);
            }
        }

        for (ProjectionPoint leftIntercept : getMapEdgeInterceptsAtX(projBB.getLowerLeftPoint().getX())) {
            if (pointIsBetween(leftIntercept, projBB.getLowerLeftPoint(), projBB.getUpperLeftPoint())) {
                intercepts.add(leftIntercept);
            }
        }

        return intercepts;
    }

    /**
     * Returns the points at which the line {@code x = x0} intersects the map edge.
     *
     * @param x0  defines a line that may intersect the map edge, in projection coordinates.
     * @return    the points at which the line {@code x = x0} intersects the map edge. May be empty.
     */
    public List<ProjectionPoint> getMapEdgeInterceptsAtX(double x0) {
        List<ProjectionPoint> mapEdgeIntercepts = new LinkedList<>();
        if (projToLatLon(x0, falseNorthing) == INVALID) {  // The line {@code x = x0} does not intersect the map.
            return mapEdgeIntercepts;  // Empty list.
        }

        double x0natural = x0 - falseEasting;
        double limitLon_r = (x0natural < 0) ? -PI : +PI;
        double deltaLon_r = limitLon_r - Math.toRadians(centMeridian);

        // This formula comes from solving 30-1 for phi, and then plugging it into 30-2. See Snyder, p 247.
        double minY = -earthRadius * Math.acos(x0natural / (earthRadius * deltaLon_r));
        double maxY = +earthRadius * Math.acos(x0natural / (earthRadius * deltaLon_r));

        mapEdgeIntercepts.add(new ProjectionPointImpl(x0, minY + falseNorthing));
        mapEdgeIntercepts.add(new ProjectionPointImpl(x0, maxY + falseNorthing));
        return mapEdgeIntercepts;
    }

    /**
     * Returns the points at which the line {@code y = y0} intersects the map edge.
     *
     * @param y0  defines a line that intersects the map edge, in projection coordinates.
     * @return    the points at which the line {@code y = y0} intersects the map edge. May be empty.
     */
    public List<ProjectionPoint> getMapEdgeInterceptsAtY(double y0) {
        List<ProjectionPoint> mapEdgeIntercepts = new LinkedList<>();
        if (projToLatLon(falseEasting, y0) == INVALID) {  // The line {@code y = y0} does not intersect the map.
            return mapEdgeIntercepts;  // Empty list.
        }

        double minX = getXAt(y0, -PI);
        double maxX = getXAt(y0, +PI);

        mapEdgeIntercepts.add(new ProjectionPointImpl(minX, y0));
        mapEdgeIntercepts.add(new ProjectionPointImpl(maxX, y0));
        return mapEdgeIntercepts;
    }

    private double getXAt(double y0, double lon_r) {
        double y0natural = y0 - falseNorthing;
        double deltaLon_r = lon_r - Math.toRadians(centMeridian);

        // This formula comes from plugging 30-6 into 30-1. See Snyder, p 247-248.
        double x = earthRadius * deltaLon_r * Math.cos(y0natural / earthRadius);

        return x + falseEasting;
    }

    private boolean pointIsBetween(ProjectionPoint point, ProjectionPoint linePoint1, ProjectionPoint linePoint2) {
        if (linePoint1.getX() == linePoint2.getX()) {  // No fuzzy comparison necessary.
            assert point.getX() == linePoint1.getX() : "point should have the same X as the line.";

            double minY = Math.min(linePoint1.getY(), linePoint2.getY());
            double maxY = Math.max(linePoint1.getY(), linePoint2.getY());

            // Returns true if point.getY() is in the range [minY, maxY], with fuzzy math.
            return DoubleMath.fuzzyCompare(minY, point.getY(), TOLERANCE) <= 0 &&
                   DoubleMath.fuzzyCompare(point.getY(), maxY, TOLERANCE) <= 0;
        } else if (linePoint1.getY() == linePoint2.getY()) {  // No fuzzy comparison necessary.
            assert point.getY() == linePoint1.getY() : "point should have the same Y as the line.";

            double minX = Math.min(linePoint1.getX(), linePoint2.getX());
            double maxX = Math.max(linePoint1.getX(), linePoint2.getX());

            // Returns true if point.getX() is in the range [minX, maxX], with fuzzy math.
            return DoubleMath.fuzzyCompare(minX, point.getX(), TOLERANCE) <= 0 &&
                   DoubleMath.fuzzyCompare(point.getX(), maxX, TOLERANCE) <= 0;
        } else {
            throw new AssertionError("CAN'T HAPPEN: linePoint1 and linePoint2 are corners on the same side of a " +
                    "bounding box; they must have *identical* x or y values.");
        }
    }

    private LatLonRect makeLatLonRect(List<ProjectionPoint> projPoints) {
        if (projPoints.isEmpty()) {
            return LatLonRect.INVALID;
        }

        double minLat = +Double.MAX_VALUE;
        double minLon = +Double.MAX_VALUE;
        double maxLat = -Double.MAX_VALUE;
        double maxLon = -Double.MAX_VALUE;

        for (ProjectionPoint projPoint : projPoints) {
            LatLonPoint latLonPoint = projToLatLon(projPoint);
            assert latLonPoint != INVALID : "We should have filtered out bad points and added good ones. WTF?";

            minLat = Math.min(minLat, latLonPoint.getLatitude());
            minLon = Math.min(minLon, latLonPoint.getLongitude());
            maxLat = Math.max(maxLat, latLonPoint.getLatitude());
            maxLon = Math.max(maxLon, latLonPoint.getLongitude());
        }

        return new LatLonRect(new LatLonPointImpl(minLat, minLon), new LatLonPointImpl(maxLat, maxLon));
    }
}


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

package ucar.nc2.grib.grib1;

import ucar.nc2.grib.GribNumbers;
import ucar.nc2.grib.GribUtils;
import ucar.nc2.grib.grib1.tables.Grib1Customizer;
import ucar.nc2.grib.grib1.tables.Grib1ParamTableReader;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.time.CalendarPeriod;
import ucar.nc2.wmo.CommonCodeTable;
import ucar.unidata.io.RandomAccessFile;

import javax.annotation.concurrent.Immutable;
import java.io.IOException;
import java.util.Formatter;
import java.util.zip.CRC32;

/**
 * The Product Definition Section for GRIB-1 files
 *
 * @author caron
 */

@Immutable
public final class Grib1SectionProductDefinition {

  private final byte[] rawData;

  /**
   * Read Product Definition section from raf.
   *
   * @param raf RandomAccessFile, with pointer at start of section
   * @throws java.io.IOException      on I/O error
   * @throws IllegalArgumentException if not a GRIB-2 record
   */
  public Grib1SectionProductDefinition(RandomAccessFile raf) throws IOException {
    int length = GribNumbers.uint3(raf);
    rawData = new byte[length];
    raf.skipBytes(-3);
    raf.readFully(rawData);
  }

  /**
   * Set PDS section from byte array.
   *
   * @param rawData the byte array
   */
  public Grib1SectionProductDefinition(byte[] rawData) {
    this.rawData = rawData;
  }

  /**
   * get the raw bytes of the PDS.
   *
   * @return PDS as byte[]
   */
  public byte[] getRawBytes() {
    return rawData;
  }

  public int getLength() {
    return GribNumbers.uint3(getOctet(1), getOctet(2), getOctet(3));
  }

  /**
   * gets the Table version (octet 4).
   *
   * @return table version
   */
  public final int getTableVersion() {
    return getOctet(4);
  }

  /**
   * Center (octet 5) common code C-1.
   *
   * @return center id
   */
  public final int getCenter() {
    return getOctet(5);
  }

  /**
   * Generating Process (octet 6).
   *
   * @return typeGenProcess
   */
  public final int getGenProcess() {
    return getOctet(6);
  }

  /**
   * Grid Definition (octet 7).
   * "Number of grid used – from catalogue defined by originating centre". So this is center dependent.
   * "Where octet 7 defines a catalogued grid, that grid should also be defined in Section 2, provided the flag in octet 8
   * indicates inclusion of Section 2.
   * Octet 7 must be set to 255 to indicate a non-catalogued grid, in which case the grid will be defined in Section 2."
   *
   * @return Grid Definition.
   */
  public final int getGridDefinition() {
    return getOctet(7);
  }

  /**
   * Flag (octet 8).
   *
   * @return Flag
   */
  public final int getFlag() {
    return getOctet(8);
  }

  /**
   * Parameter number (octet 9) - code table 2.
   *
   * @return index number of parameter in table
   */
  public final int getParameterNumber() {
    return getOctet(9);
  }

  /**
   * Level type (octet 10) - code table 3.
   *
   * @return level type
   */
  public final int getLevelType() {
    return getOctet(10);
  }

  /**
   * Level value1 (octet 11).
   *
   * @return level value1
   */
  public final int getLevelValue1() {
    return getOctet(11);
  }

  /**
   * Level value2 (octet 12).
   *
   * @return level value2
   */
  public final int getLevelValue2() {
    return getOctet(12);
  }

  /**
   * Reference Date (octet 13-17).
   * Reference time of data – date and time of start of averaging or accumulation period.
   *
   * @return Reference Date as CalendarDate.
   */
  public final CalendarDate getReferenceDate() {
    int century = getReferenceCentury() - 1;
    if (century == -1) century = 20;

    int year = getOctet(13);
    int month = getOctet(14);
    int day = getOctet(15);
    int hour = getOctet(16);
    int minute = getOctet(17);
    return CalendarDate.of(null, century * 100 + year, month, day, hour, minute, 0);
  }

  /**
   * Time unit (octet 18) - code table 4. Same as GRIB2 Table 4.4
   *
   * @return time unit
   */
  public final int getTimeUnit() {
    return getOctet(18);
  }

  /**
   * Time value 1 (octet 19).
   * Period of time (number of time units) (0 for analyses or initialized analyses).
   * Units of time given by octet 18
   *
   * @return time value 1
   */
  public final int getTimeValue1() {
    return getOctet(19);
  }

  /**
   * Time value 2 (octet 20).
   * Period of time (number of time units); or Time interval between successive analyses,
   * initialized analyses or forecasts, undergoing averaging or accumulation.
   * Units of time given by octet 18
   *
   * @return time value 2
   */
  public final int getTimeValue2() {
    return getOctet(20);
  }

  /**
   * Time range indicator (octet 21) - code table 5.
   *
   * @return Time range indicator
   */
  public final int getTimeRangeIndicator() {
    return getOctet(21);
  }

  /**
   * Number included in statistics (octet 22-23).
   * Number included in calculation when octet 21 (Code table 5) refers to a statistical
   * process, such as average or accumulation; otherwise set to zero
   *
   * @return Number included in statistics
   */
  public final int getNincluded() {
    return GribNumbers.int2(getOctet(22), getOctet(23));
  }

  /**
   * Number missing in statistics (octet 24).
   * Number missing from calculation in case of statistical process
   *
   * @return Number missing in statistics
   */
  public final int getNmissing() {
    return getOctet(24);
  }

  /**
   * Century of reference (octet 25).
   *
   * @return Century of reference
   */
  public final int getReferenceCentury() {
    return getOctet(25);
  }

  /**
   * Center (octet 26) common code C-12.
   *
   * @return subcenter id
   */
  public final int getSubCenter() {
    return getOctet(26);
  }

  /**
   * Units decimal scale factor (octet 27-28) .
   *
   * @return Units decimal scale factor
   */
  public final int getDecimalScale() {
    return GribNumbers.int2(getOctet(27), getOctet(28));
  }

  /**
   * Check if GDS exists from the flag.
   *
   * @return true, if GDS exists
   */
  public final boolean gdsExists() {
    return (getFlag() & 128) == 128;
  }

  /**
   * Check if BMS exists from the flag
   *
   * @return true, if BMS exists
   */
  public final boolean bmsExists() {
    return (getFlag() & 64) == 64;
  }

  /**
   * Get the indexth byte in the PDS as an integer.
   * THIS IS ONE BASED (not zero) to correspond with the manual
   *
   * @param index 1 based index
   * @return rawData[index-1] & 0xff
   */
  private int getOctet(int index) {
    if (index > rawData.length) return GribNumbers.UNDEFINED;
    return rawData[index - 1] & 0xff;
  }

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

  private String getCalendarPeriodAsString() {
    try {
      return GribUtils.getCalendarPeriod(getTimeUnit()).toString();
    } catch (UnsupportedOperationException e) {
      return "Unknown Time Unit";
    }
  }

  public void showPds(Grib1Customizer cust, Formatter f) {

    f.format("5           Originating Center : (%d) %s%n", getCenter(), CommonCodeTable.getCenterName(getCenter(), 1));
    f.format("26       Originating SubCenter : (%d) %s%n", getSubCenter(), cust.getSubCenterName(getSubCenter()));
    f.format("4                Table Version : %d%n", getTableVersion());

    Grib1Parameter parameter = cust.getParameter(getCenter(), getSubCenter(), getTableVersion(), getParameterNumber());
    if (parameter != null) {
      Grib1ParamTableReader ptable = parameter.getTable();
      f.format("               Parameter Table : (%d-%d-%d) %s%n", getCenter(), getSubCenter(), getTableVersion(), (ptable == null) ? "MISSING" : ptable.getPath());
      f.format("                Parameter Name : (%d) %s%n", getParameterNumber(), parameter.getName());
      f.format("                Parameter Desc : %s%n", parameter.getDescription());
      f.format("               Parameter Units : %s%n", parameter.getUnit());
    } else {
      f.format("               Parameter %d not found%n", getParameterNumber());
    }

    f.format("6      Generating Process Type : (%d) %s%n", getGenProcess(), cust.getGeneratingProcessName(getGenProcess()));
    f.format("7              Grid Definition : (%d) %n", getGridDefinition());
    f.format("8                         Flag : (%d) %n", getFlag());

    f.format("13-17           Reference Time : %s%n", getReferenceDate());
    f.format("18                  Time Units : (%d) %s%n", getTimeUnit(), getCalendarPeriodAsString());
    Grib1ParamTime ptime = cust.getParamTime(this);
    f.format("19                 Time 1 (P1) : %d%n", getTimeValue1());
    f.format("20                 Time 2 (P2) : %d%n", getTimeValue2());
    f.format("21        Time Range Indicator : (%d) %s%n", getTimeRangeIndicator(), ptime.getTimeTypeName());
    f.format("22-23           N in statistic : (%d)%n", getNincluded());
    f.format("24                   N missing : (%d)%n", getNmissing());
    f.format("                   Time  coord : %s == %s%n", ptime.getTimeCoord(), makeTimeCoord(ptime));
    Grib1ParamLevel plevel = cust.getParamLevel(this);
    f.format("10                  Level Type : (%d) %s%n", getLevelType(), plevel.getNameShort());
    f.format("             Level Description : %s%n", plevel.getDescription());
    f.format("                 Level Value 1 : %f%n", plevel.getValue1());
    f.format("                 Level Value 2 : %f%n", plevel.getValue2());
    f.format("27-28     Decimal Scale Factor : %d%n", getDecimalScale());

    f.format("                    GDS Exists : %s%n", gdsExists());
    f.format("                    BMS Exists : %s%n", bmsExists());
  }

  private String makeTimeCoord(Grib1ParamTime ptime) {
    CalendarPeriod period = GribUtils.getCalendarPeriod(getTimeUnit());
    CalendarDateUnit unit = CalendarDateUnit.of(null, period.getField(), getReferenceDate());
    int timeCoord;
    if (ptime.isInterval()) {
      int[] intv = ptime.getInterval();
      CalendarDate cdate1 = unit.makeCalendarDate(period.getValue() * intv[0]);
      CalendarDate cdate2 = unit.makeCalendarDate(period.getValue() * intv[1]);
      return "(" + cdate1.toString() + "," + cdate2.toString() + ")";
    } else {
      timeCoord = ptime.getForecastTime();
      CalendarDate cdate = unit.makeCalendarDate(period.getValue() * timeCoord);
      return cdate.toString();
    }
  }

  ////////////////////////////////////////////////////////
  // Ensembles

/* http://www.ecmwf.int/publications/manuals/d/gribapi/fm92/grib1/show/local/
   1)
   see " local definition # 1"
   from Ernst de Vreede via Jitka 01/31/2012
  ECMWF operational Ensemble Prediction System (EPS) have extra information in the PDS:
    Octet 41 = 1: ECMWF local GRIB extension: 1 = Mars labelling or ensemble forecast data&
    Octet 42 = 1: ECMWF GRIB class, 1=operational data
    Octet 43 = 10/11: ECMWF GRIB type: 10=Control forecast, 11=Perturbed forecast
    Octet 44,45 = 1035, ECMWF GRIB stream 1035 = Ensemble Prediction System
    Octet 50 = perturbationNumber (0 for control, 1-50 for perturbed forecasts)
    Octet 51 = numberOfForecastsInEnsemble (0 if not ensemble, 1-50 for perturbed forecasts)

  The really distinguishing octets are: octet 41 =1, octet 42=1, octet 43=10 or 11, octet 51>0 with octet 50 giving the perturbation number.
  The other parameters octets 42 (operational stream) and octet 44/45 might be too specific, might narrow down too much.

  2)
  http://www.ecmwf.int/publications/manuals/d/gribapi/fm92/grib1/detail/local/30/
*/

  /*
   * NCEP Appendix C Manual 388
   * http://www.nco.ncep.noaa.gov/pmb/docs/on388/appendixc.html
   * states that if the PDS is > 28 bytes and octet 41 == 1
   * then it's an ensemble an product.
   */

  public boolean isEnsemble() {
    switch (getCenter()) {
      case 7:
        return ((rawData.length >= 43) && (getOctet(41) == 1));

      case 98:
        return ((rawData.length >= 51) &&
                (getOctet(41) == 1 || getOctet(41) == 30) &&
                (getOctet(43) == 10 || getOctet(43) == 11));
    }
    return false;
  }

  public final int getPerturbationType() {
    if (!isEnsemble()) return GribNumbers.UNDEFINED;
    switch (getCenter()) {
      case 7:
        return getOctet(42);
      case 98:
        return getOctet(43);
    }
    return GribNumbers.UNDEFINED;
  }

  public final int getPerturbationNumber() {
    if (!isEnsemble()) return GribNumbers.UNDEFINED;

    switch (getCenter()) {
      case 7: {
        int type = getOctet(42);
        int id = getOctet(43);
        if (type == 1) return 0;
        if (type == 2) return id;
        if (type == 3) return 5 + id;
      }
      case 98:
        return getOctet(50);
    }
    return GribNumbers.UNDEFINED;
  }

  public long calcCRC() {
    CRC32 crc32 = new CRC32();
    crc32.update(rawData);
    return crc32.getValue();
  }

}


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

package ucar.nc2.ft.point.standard.plug;

import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.ft.point.standard.*;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.Variable;
import ucar.nc2.Structure;
import ucar.nc2.Dimension;
import ucar.ma2.DataType;

import java.util.Formatter;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;

/**
 * FLS Wind profile data
 * @author caron
 * @since Apr 23, 2008
 */
public class FslWindProfiler extends TableConfigurerImpl  {

    // :title = "WPDN data : selected by ob time : time range from 1207951200 to 1207954800";
  public boolean isMine(FeatureType wantFeatureType, NetcdfDataset ds) {
    String title = ds.findAttValueIgnoreCase(null, "title", null);
    if (title == null) {
        title = ds.findAttValueIgnoreCase(null, "DD_reference", null);
        if(title != null) {
            title = ds.findVariable("staLat") != null ? title : null;
        }
    }
    return title != null && (title.startsWith("WPDN data") || title.startsWith("RASS data") || title.contains("88-21-R2"));
  }

  public TableConfig getConfig(FeatureType wantFeatureType, NetcdfDataset ds, Formatter errlog) throws IOException {
    String title = ds.findAttValueIgnoreCase(null, "title", null);
    if (title == null)
        title = ds.findAttValueIgnoreCase(null, "DD_reference", null);
    assert (title != null);

    boolean isRass = title.startsWith("RASS data");
    String xml = isRass ? "resources/nj22/pointConfig/FslRassProfiler.xml" : "resources/nj22/pointConfig/FslWindProfiler.xml";

    PointConfigXML reader = new PointConfigXML();
    TableConfig tc = reader.readConfigXMLfromResource(xml, wantFeatureType, ds, errlog);

    for (TableConfig inner : tc.children.get(0).children)
      makeMultidimInner(ds, tc, inner, inner.outerName, inner.innerName);
    return tc;
  }

  private void makeMultidimInner(NetcdfDataset ds, TableConfig parentTable, TableConfig childTable, String outerDin, String innerDim) {
    Dimension parentDim = ds.findDimension(outerDin);
    Dimension childDim = ds.findDimension(innerDim);

    // divide up the variables between the parent and the child
    List<String> obsVars;
    List<Variable> vars = ds.getVariables();
    List<String> parentVars = new ArrayList<>(vars.size());
    obsVars = new ArrayList<>(vars.size());
    for (Variable orgV : vars) {
      if (orgV instanceof Structure) continue;

      Dimension dim0 = orgV.getDimension(0);
      if ((dim0 != null) && dim0.equals(parentDim)) {
        if ((orgV.getRank() == 1) || ((orgV.getRank() == 2) && orgV.getDataType() == DataType.CHAR)) {
          parentVars.add(orgV.getShortName());
        } else {
          Dimension dim1 = orgV.getDimension(1);
          if ((dim1 != null) && dim1.equals(childDim))
            obsVars.add(orgV.getShortName());
        }
      }
    }
    parentTable.vars = parentVars;
    childTable.vars = obsVars;
  }

  /* public TableConfig getConfig(FeatureType wantFeatureType, NetcdfDataset ds, Formatter errlog) {
    //TableConfig nt = new TableConfig(Table.Type.Singleton, "station");

    TableConfig profile = new TableConfig(Table.Type.Structure, "record");
    profile.structName = "record";
    profile.dimName = Evaluator.getDimensionName(ds, "recNum", errlog);
    profile.time = Evaluator.getVariableName(ds, "timeObs", errlog);

    profile.stnId = Evaluator.getVariableName(ds, "staName", errlog);
    profile.stnWmoId = Evaluator.getVariableName(ds, "wmoStaNum", errlog);
    profile.lat = Evaluator.getVariableName(ds, "staLat", errlog);
    profile.lon = Evaluator.getVariableName(ds, "staLon", errlog);
    // profile.elev = Evaluator.getVariableName(ds, "staElev", errlog);

    //profile.join = new TableConfig.JoinConfig(Join.Type.Identity);
    profile.featureType = FeatureType.PROFILE;
    // nt.addChild(profile);

    TableConfig levels = new TableConfig(Table.Type.MultidimInner, "levels");
    levels.outerName = profile.dimName;
    levels.innerName = Evaluator.getDimensionName(ds, "level", errlog);
    levels.elev = Evaluator.getVariableName(ds, "level", errlog);

    profile.addChild(levels);

    Dimension outer = Evaluator.getDimension(ds, "recNum", errlog);
    Dimension inner = Evaluator.getDimension(ds, "level", errlog);

    // divide up the variables between the parent and the obs
    List<String> obsVars = null;
    List<Variable> vars = ds.getVariables();
    List<String> parentVars = new ArrayList<String>(vars.size());
    obsVars = new ArrayList<String>(vars.size());
    for (Variable orgV : vars) {
      if (orgV instanceof Structure) continue;

      Dimension dim0 = orgV.getDimension(0);
      if ((dim0 != null) && dim0.equals(outer)) {
        if ((orgV.getRank() == 1) || ((orgV.getRank() == 2) && orgV.getDataType() == DataType.CHAR)) {
          parentVars.add(orgV.getShortName());
        } else {
          Dimension dim1 = orgV.getDimension(1);
          if ((dim1 != null) && dim1.equals(inner))
            obsVars.add(orgV.getShortName());
        }
      }
    }
    profile.vars = parentVars;
    levels.vars = obsVars;

    return profile;
  }   */

}