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

package ucar.util.prefs.ui;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import ucar.util.prefs.PersistenceManager;
import ucar.util.prefs.PreferencesExt;

import javax.swing.*;
import javax.swing.event.EventListenerList;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.*;
import java.util.List;
import java.util.prefs.Preferences;

/**
 *  Create a User Preferences Panel or Dialog.
 *
 *  A PrefPanel manages a set of Fields with convenience methods for rapidly creating
 *  User Dialogs whose values are made persistent by a Preferences Store. All Fields
 *  contained in the PrefPanel share the same Preferences, and so must have unique names.
 *
 * Send ActionEvent when "accept" button is pressed. Can also listen on individual Fields.
 * You must call one finish() method exactly once, when you are done adding Fields.
 *
 *
 Example of use:

    PreferencesExt store = null;
    try {
      xstore = XMLStore.createFromFile("E:/dev/prefs/test/panel/panel.xml", null);
      store = xstore.getPreferences();
    } catch( Exception e) {
      System.out.println(e);
      System.exit( 1);
    }
    PrefPanel pp = new PrefPanel("test", store);
    pp.addTextField("name", "name", "defValue");
    pp.newColumn();
    pp.addTextField("name2", "name2", "defValue22");
    pp.newColumn();
    pp.addTextField("name3", "name3", "defValue22 asd jalskdjalksjd");
    pp.finish();

    pp.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        // accept was called
      }
    });

 *
 *
 Form layout

 * The PrefPanel is layed out with the jgoodies FormLayout layout manager and PanelBuilder, which use a row, colummn grid.
 * Fields in the same column are aligned.
 *
 *
There are 2 ways to do form layout: implicit and explicit. With implicit, the position is implicitly specified by the
 *  order the fields are added, using, for example:
 *
  addDoubleField(String fldName, String label, double defValue)

 *
 * The fields are all added in a column. To start a new column, use setCursor().
 *
With explicit, you specify the row and col, and an optional constraint:
 *
  addDoubleField(String fldName, String label, double defValue, int col, int row, String constraint)

 *
 * Row and column numbers are 0 based. Each field has a width of 2 columns (one for the label and one for the
 *  component) and a height of 1 row, unless you specify otherwise using a constraint.
 * A heading takes up an entire row, spanning all columns
 *
 * @author John Caron
 */

public class PrefPanel extends JPanel {
  //static private KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();

  private String name;
  private Preferences prefs;
  private PersistenceManager storeData;

  private boolean finished = false;
  private HashMap<String, Field> flds = new HashMap<>(40);
  // private ArrayList currentComps, colList; // track columns of components
  private List<LayoutComponent> layoutComponents; // use with form layout
  private int cursorRow = 0, cursorCol = 0; // current row and column

  private JPanel mainPanel;
  private List<JComponent> auxButtons = new ArrayList<>();

  // event handling
  private EventListenerList listenerList = new EventListenerList();

  private boolean debugLayout = false;

  /**
   * Constructor.
   * @param name may be null.
   * @param prefs keep values in here; may be null.
   */
  public PrefPanel(String name, PreferencesExt prefs) {
    this( name, prefs, prefs);
  }
  /**
   * Constructor.
   * @param name may be null.
   * @param storeData keep values in here; may be null.
   */
  public PrefPanel(String name, Preferences prefs, PersistenceManager storeData) {
    this.name = name;
    this.prefs = prefs;
    this.storeData = storeData;

    //colList = new ArrayList( 5);
    //currentComps = new ArrayList( 10);
    //colList.add( currentComps);
    layoutComponents = new ArrayList<>(20);

    /* manager.addPropertyChangeListener( "focusOwner", new PropertyChangeListener() {
       public void propertyChange(PropertyChangeEvent evt) {
         Object val = evt.getNewValue();
         String sval = (val == null) ? "null" : val.getClass().getName();
         Component own = manager.getFocusOwner();
         String sown = (own == null) ? "null" : own.getClass().getName();
         System.out.println("focusOwner val="+sval+" own="+sown);
       }
     });
    manager.addPropertyChangeListener( "permanentFocusOwner", new PropertyChangeListener() {
       public void propertyChange(PropertyChangeEvent evt) {
         Object val = evt.getNewValue();
         String sval = (val == null) ? "null" : val.getClass().getName();
         Component pown = manager.getPermanentFocusOwner();
         String sown = (pown == null) ? "null" : pown.getClass().getName();
         System.out.println("permanentFocusOwner val="+sval+" own="+sown);
       }
     }); */
   }

  /* public void setPersistenceManager (PersistenceManager storeData) {
    this.storeData = storeData;
    Iterator iter = flds.values().iterator();
    while (iter.hasNext()) {
      Field f = (Field) iter.next();
      f.setPersistenceManager(storeData);
    }
  }       */

  /** Add listener: action event sent if "apply" button is pressed */
  public void addActionListener(ActionListener l) {
    listenerList.add(java.awt.event.ActionListener.class, l);
  }
  /** Remove listener */
  public void removeActionListener(ActionListener l) {
    listenerList.remove(java.awt.event.ActionListener.class, l);
  }

  private void fireEvent(java.awt.event.ActionEvent event) {
    // Guaranteed to return a non-null array
    Object[] listeners = listenerList.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    for (int i = listeners.length-2; i>=0; i-=2) {
      ((java.awt.event.ActionListener)listeners[i+1]).actionPerformed(event);
    }
  }

  /** Call Field.accept() on all Fields. This puts any edits into the Store,
   *  and fires PropertyChangeEvents if any values change, and sends an
   *  ActionEvent to any listeners.
   */
  public boolean accept() {
    StringBuffer buff = new StringBuffer("Invalid field value ");
    boolean ok = true;
    for (Object o : flds.values())
      ok &= ((Field) o).accept(buff);

    if (!ok) {
      try { JOptionPane.showMessageDialog(PrefPanel.findActiveFrame(), buff.toString()); }
      catch (HeadlessException e) { }
      return false;
    }

    /* store the text widths if they exist
    if (storeData != null) {
      Preferences substore = prefs.node("sizes");
      iter = flds.values().iterator();
      while (iter.hasNext()) {
        Field fld = (Field) iter.next();
        JComponent comp = fld.getEditComponent();
        substore.putInt(fld.getName(), (int) comp.getPreferredSize().getWidth());
      }
    } */
    fireEvent(new ActionEvent(this, 0, "Accept"));
    return true;
  }

  /**
   * Set enabled on all the fields in the prefPanel
   * @param enable enable if true
   */
  public void setEnabled( boolean enable) {
    for (Field field : flds.values())
      field.setEnabled(enable);
  }

  /** Return the name of the PrefPanel. */
  public String getName() { return name; }

  /** Iterator over the fields */
  public Iterator<Field> getFields() {
    return flds.values().iterator();
  }

  /**
   * Find the field with the specified name.
   * @param name of Field
   * @return Field or null if not found
   */
  public Field getField(String name) {
    Field fld = flds.get(name);
    if (fld == null) return null;
    return (fld instanceof FieldResizable) ? ((FieldResizable)fld).getDelegate() : fld;
  }

  /**
   * Get current value of the named field
   * @param name of field
   * @return value of named field
   */
  public Object getFieldValue(String name) {
    Field fld = getField(name);
    if (fld == null) throw new IllegalArgumentException("no field named "+name);
    return fld.getValue();
  }

  /**
   * Set the current value of the named field
   * @param name of field
   * @param value of field
   */
  public void setFieldValue(String name, Object value) {
    Field fld = getField(name);
    if (fld == null) throw new IllegalArgumentException("no field named "+name);
    fld.setValue(value);
  }

  /** Add a button to the button panel */
  public void addButton( JComponent b) { auxButtons.add(b); }

  /**
   * Add a field created by the user.
   * @param fld add this field.
   */
  public Field addField(Field fld) {
    addField( fld, cursorCol, cursorRow, null);
    cursorRow++;
    return fld;
  }

  public Field addField(Field fld, int col, int row, String constraint) {
    if (null != flds.get(fld.getName()))
      throw new IllegalArgumentException("PrefPanel: already have field named "+fld.getName());

    //currentComps.add( fld);
    flds.put( fld.getName(), fld);
    layoutComponents.add( new LayoutComponent( fld, col, row, constraint));

    fld.addPropertyChangeListener(new PropertyChangeListener() {
      public void propertyChange( PropertyChangeEvent e) { revalidate(); }
    });

    return fld;
  }

  public Field.BeanTableField addBeanTableField(String fldName, String label, java.util.ArrayList beans,
                                                Class beanClass, int col, int row, String constraint) {
    Field.BeanTableField fld = new Field.BeanTableField(
            fldName, label, beans, beanClass, (PreferencesExt) prefs, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  /**
   * Add a boolean field as a checkbox.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   */
  public Field.CheckBox addCheckBoxField(String fldName, String label, boolean defValue) {
    Field.CheckBox fld = new Field.CheckBox(fldName, label, defValue, storeData);
    addField( fld);
    return fld;
  }

  public Field.CheckBox addCheckBoxField(String fldName, String label, boolean defValue, int col, int row) {
    Field.CheckBox fld = new Field.CheckBox(fldName, label, defValue, storeData);
    addField( fld, col, row, null);
    return fld;
  }

  /**
   * add a boolean field to turn a field on/off
   * @param fldName: the name to store the data in the PersistenceManagerData
   * @param defvalue: default value
   * @param enabledField: the InputField to enable/disable; must already be added
   *
  public Field.BooleanEnabler addEnablerField(String fldName, boolean defValue, Field enabledField) {
    Field.BooleanEnabler enabler = new Field.BooleanEnabler(fldName, defValue, enabledField, storeData);
    // flds.add( enabledField);
    enabledField.hasEnabler = true;

    flds.add( enabler);
    return enabler;
  } */

  /**
   * Add a field that edits a date
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   */
  public Field.Date addDateField(String fldName, String label, Date defValue) {
    Field.Date fld = new Field.Date(fldName, label, defValue, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  }

  public Field.Date addDateField(String fldName, String label, Date defValue,
        int col, int row, String constraint) {
    Field.Date fld = new Field.Date(fldName, label, defValue, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  /**
   * Add a field that edits a double
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   */
  public Field.Double addDoubleField(String fldName, String label, double defValue) {
    Field.Double fld = new Field.Double(fldName, label, defValue, -1, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  }

  public Field.Double addDoubleField(String fldName, String label, double defValue,
        int col, int row, String constraint) {
    Field.Double fld = new Field.Double(fldName, label, defValue, -1, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  public Field.Double addDoubleField(String fldName, String label, double defValue, int nfracDig,
        int col, int row, String constraint) {
    Field.Double fld = new Field.Double(fldName, label, defValue, nfracDig, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  public Field.EnumCombo addEnumComboField(String fldName, String label, java.util.Collection defValues,
               boolean editable, int col, int row, String constraint) {
    Field.EnumCombo fld = new Field.EnumCombo(fldName, label, defValues, storeData);
    addField( fld, col, row, constraint);
    fld.setEditable( editable);
    return fld;
  }

  public Field.EnumCombo addEnumComboField(String fldName, String label, java.util.Collection defValues, boolean editable) {
    Field.EnumCombo fld = new Field.EnumCombo(fldName, label, defValues, storeData);
    addField( fld);
    fld.setEditable( editable);
    return fld;
  }

  /**
   * Add a field that edits a formatted text field
   * NOTE: to use this directly, you must use a PersistenceManagerExt object.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   *
  public Field.TextFormatted addTextFormattedField(String fldName, String label,
         JFormattedTextField tf, Object defValue) {
    Field.TextFormatted fld = new Field.TextFormatted(fldName, label, tf, defValue, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  } */

  /**
   * Add a field that edits an integer
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   */
  public Field.Int addIntField(String fldName, String label, int defValue) {
    Field.Int fld = new Field.Int(fldName, label, defValue, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  }

  /**
   * Add an integer field with units.
   * @param fldName: the name to store the data in the PersistenceManagerData
   * @param label: used as the label on the panel
   * @param defvalue: default value
   * @param units: optional unit label
   *
  public Field.Int addIntField(String fldName, String label, int defValue, String units) {
    Field.Int fld = new Field.IntUnits(fldName, label, units, defValue, storeData);
    flds.add( fld);
    return fld;
  } */

  /**
   * Add a password text field.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   */
  public Field.Password addPasswordField(String fldName, String label, String defValue) {
    Field.Password fld = new Field.Password(fldName, label, defValue, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  }

  /**
   * Add a text field.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValue default value
   * @return the Field.Text object that was added
   */
  public Field.Text addTextField(String fldName, String label, String defValue) {
    Field.Text fld = new Field.Text(fldName, label, defValue, storeData);
    addField( new FieldResizable(fld, this));
    return fld;
  }

  public Field.Text addTextField(String fldName, String label, String defValue, int col, int row, String constraint ) {
    Field.Text fld = new Field.Text(fldName, label, defValue, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  /**
   * Add a text combobox field.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param defValues list of default values (Strings) to include in the comboBox. May be null.
   *    These are added to the combobox (at the end) no matter how many there are.
   * @param nKeep number of most recently used values to keep
   * @param editable whether the user can add new entries the list to select from.
   */
  public Field.TextCombo addTextComboField(String fldName, String label, java.util.Collection defValues, int nKeep, boolean editable) {
    Field.TextCombo fld = new Field.TextCombo(fldName, label, defValues, nKeep, storeData);
    addField( fld);
    fld.setEditable( editable);
    return fld;
  }

  public Field.TextCombo addTextComboField(String fldName, String label, java.util.Collection defValues, int nKeep,
               boolean editable, int col, int row, String constraint) {
    Field.TextCombo fld = new Field.TextCombo(fldName, label, defValues, nKeep, storeData);
    addField( fld, col, row, constraint);
    fld.setEditable( editable);
    return fld;
  }

  /**
   * Add a TextArea field.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   * @param def default value
   * @param nrows number of rows
   */
  public Field.TextArea addTextAreaField(String fldName, String label, String def, int nrows) {
    Field.TextArea fld = new Field.TextArea(fldName, label, def, nrows, storeData);
    addField( fld);
    return fld;
  }

  public Field.TextArea addTextAreaField(String fldName, String label, String def, int nrows,
                                         int col, int row, String constraint) {
    Field.TextArea fld = new Field.TextArea(fldName, label, def, nrows, storeData);
    addField( fld, col, row, constraint);
    return fld;
  }

  /**
   * Add a text combobox field.
   * @param fldName the name to store the data in the PersistenceManagerData
   * @param label used as the label on the panel
   *  @param defValues list of default values to include in the comboBox. May be null.
   *    These are added to the combobox (at the end) no matter how many there are.
   *  @param nKeep number of most recently used values to keep
   *
  public Field.Combo addComboField(String fldName, String label, java.util.Collection defValues, int nKeep) {
    Field.Combo fld = new Field.Combo(fldName, label, defValues, nKeep, (PersistenceManagerExt) storeData);
    addField( fld);
    return fld;
  } */

  /** Add a heading that takes no input */
  public void addHeading(String heading) {
    addHeading(heading, cursorRow);
    cursorRow++;
  }

  /** Add a heading at the specified row. this spans all columns */
  public void addHeading(String heading, int row) {
    layoutComponents.add( new LayoutComponent( heading, 0, row, null));
  }

  /** Add a Component. */
  public void addComponent(Component comp, int col, int row, String constraint) {
    layoutComponents.add( new LayoutComponent( comp, col, row, constraint));
  }

  /** Add a seperator after the last field added. */
  public void addSeparator() {
    addEmptyRow( cursorRow++, 15);
  }

  /** Add a seperator after the last field added. */
  public void addEmptyRow(int row, int size) {
    layoutComponents.add( new LayoutComponent( null, size, row, null));
  }

  /** Start a new column.
   *  Everything added goes into a vertical column until another call to newColumn().
   */
  public void setCursor(int col, int row) {
    //currentComps = new ArrayList(10);
    //colList.add( currentComps);
    cursorCol = col;
    cursorRow = row;
  }

  /** Call this when you have finish constructing the panel, adding buttons in default spot */
  public void finish() { finish( true); }

  /**
   * Call this when you have finish constructing the panel.
   * @param addButtons if true, add buttons in default spot
   */
  public void finish( boolean addButtons) { finish( addButtons, BorderLayout.SOUTH); }

  /**
   * Call this when you have finish constructing the panel.
   * @param addButtons if true, add buttons
   * @param where to add the buttons BorderLayout.NORTH, SOUTH, EAST, WEST, CENTER
   *
  public void finish( boolean addButtons, String where) {
    if (finished)
      throw new IllegalStateException("PrefPanel "+name+": already called finish()");

    mainPanel = new JPanel();
    mainPanel.setLayout( new LayoutM("main"));

    // fetch the stored widths if they exists
    if (storeData != null) {
      Preferences substore = prefs.node("sizes");
      Iterator iter = flds.values().iterator();
      while (iter.hasNext()) {
        Field fld = (Field) iter.next();
        int width = substore.getInt(fld.getName(), 0);
        if (width > 0) {
          JComponent comp = fld.getEditComponent();
          Dimension prefDim = comp.getPreferredSize();
          // System.out.println("pref = "+prefDim+" "+width);
          comp.setPreferredSize(new Dimension( width, (int) prefDim.getHeight()));
        }
      }
    }

    // make each JPanel column
    boolean multiColumn = colList.size() > 1;
    int count = 0;
    Iterator iter = colList.iterator();
    JPanel lastPanel = null;
    while (iter.hasNext()) {
      //if (!first)
      //  mainPanel.add( new JSeparator( SwingConstants.VERTICAL));
      //first = false;

      ArrayList cs = (ArrayList) iter.next();
      JPanel panel =  makeColumnPanel( "panel"+count, cs);
      if (multiColumn)
        mainPanel.add( panel, new LayoutM.Constraint( lastPanel, 10, 0));
      else
        mainPanel = panel;

      lastPanel = panel;
      count++;
    }

      // button panel
    JPanel buttPanel = new JPanel();
    JButton acceptButton = new JButton("Apply");
    buttPanel.add(acceptButton, null);
    for (int i=0; i<auxButtons.size(); i++)
      buttPanel.add((JComponent) auxButtons.get(i), null);

      // button listeners
    acceptButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        accept();
      }
    });

    /* if (helpTarget != null) {
      JButton helpButton = new JButton("Help");
      buttPanel.add(helpButton, null);
      helpButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          System.out.println(" call help taget = "+ helpTarget);
          ucar.unidata.ui.Help.getDefaultHelp().gotoTarget(helpTarget);
          System.out.println(" complete help taget = "+ helpTarget);
        }
      });
    }

    //BoxLayout bl2 = new BoxLayout( this, BoxLayout.Y_AXIS);
    setLayout( new BorderLayout());
    add(mainPanel, BorderLayout.CENTER);

    if (addButtons) {
      if (where == BorderLayout.SOUTH) {
        JPanel south = new JPanel();
        south.setLayout(new BoxLayout( south, BoxLayout.Y_AXIS));
        south.add( new JSeparator(SwingConstants.HORIZONTAL));
        south.add( buttPanel);
        add( south, BorderLayout.SOUTH);
      } else
        add(buttPanel, where);
    }

    finished = true;
  }

  // layout a column, based on maximum label width
  private JPanel makeColumnPanel(String name, ArrayList comps) {
    JPanel parent = new JPanel( new LayoutM(name));

    // find max widths
    double maxLabelWidth = 0.0;
    double maxFieldWidth = 0.0;
    for (int i = 0; i < comps.size(); i++) {
      Object o = comps.get(i);
      if (o instanceof JLabel) {
        JLabel lab = (JLabel) o;
        Dimension prefDim = lab.getPreferredSize();
        maxLabelWidth = Math.max( maxLabelWidth, prefDim.getWidth());
      } else if (o instanceof Field) {
        Field fld = (Field) o;
        JLabel lab = new JLabel(fld.getLabel()+" :");
        Dimension prefDim = lab.getPreferredSize();
        maxLabelWidth = Math.max( maxLabelWidth, prefDim.getWidth());
        prefDim = fld.getEditComponent().getPreferredSize();
        maxFieldWidth = Math.max( maxFieldWidth, prefDim.getWidth());
      }
    }
    int totalWidth = (int) (maxLabelWidth+maxFieldWidth);

    // do the layout; the labels are right justified, the fields left justified
    ArrayList sepFlds = new ArrayList();
    JComponent lastComp = null;
    for (int i = 0; i < comps.size(); i++) {
      Object o = comps.get(i);

      if (o instanceof JLabel) {
        JLabel lab = (JLabel) o;
        parent.add( lab, new LayoutM.Constraint(lastComp, (int) -maxLabelWidth-20, 10));
        lastComp = lab;

      } else if (o instanceof Field) {
        Field fld = (Field) o;
        JLabel lab = new JLabel(fld.getLabel()+" :");
        JComponent comp = fld.getEditComponent();

        parent.add( lab, new LayoutM.Constraint(lastComp, (int) -maxLabelWidth, 10));
        lastComp = lab;
        parent.add( comp, new LayoutM.Constraint(lastComp, 5, 0));

      }  else if (o instanceof JSeparator) {
        JSeparator sep = (JSeparator) o;
        parent.add( sep, new LayoutM.Constraint(lastComp, (int) -totalWidth, 10));
        lastComp = sep;

        Dimension prefDim = sep.getPreferredSize();
        sep.setPreferredSize(new Dimension( totalWidth, (int) prefDim.getHeight()));
      }

    }

    return parent;
  } */

  /**
   * Call when finished adding components to the PrefPanel.
   * @param addButtons if true, add buttons
   * @param where BorderLayout.NORTH, SOUTH, EAST, WEST
   */
  public void finish( boolean addButtons, String where) {
    if (finished)
      throw new IllegalStateException("PrefPanel "+name+": already called finish()");

    StringBuilder sbuff = new StringBuilder();

    // column layout, first sort by col
    Collections.sort(layoutComponents, new Comparator<LayoutComponent>() {
      public int compare(LayoutComponent o1, LayoutComponent o2) {
        return o1.col - o2.col;
      }
      public boolean equals(Object o1) { return o1 == this; }
    });

    // now create column layout spec and x cell constraint
    sbuff.setLength(0);
    int currCol = -1;
    Iterator iter = layoutComponents.iterator();
    while (iter.hasNext()) {
      LayoutComponent lc = (LayoutComponent) iter.next();
      if (lc.col > currCol) {
        if (currCol >= 0)
          sbuff.append(", 5dlu, ");
        else
          sbuff.append("3dlu, ");
        sbuff.append( "right:default, 3dlu, default:grow");
        currCol += 2;
      }
      lc.ccLabel.gridX = 2*lc.col+2;
      lc.cc.gridX = 2*lc.col+4;
    }
    String colSpec = sbuff.toString();
    if (debugLayout) System.out.println(" column layout = "+ colSpec);
    int ncols = 2*currCol;

    // row layout, first sort by row
    Collections.sort(layoutComponents, new Comparator<LayoutComponent>() {
      public int compare(LayoutComponent o1, LayoutComponent o2) {
        return o1.row - o2.row;
      }
      public boolean equals(Object o1) { return o1 == this; }
    });

    // now adjust for any headings, put into y cell constraint
    int incr = 0;
    iter = layoutComponents.iterator();
    while (iter.hasNext()) {
      LayoutComponent lc = (LayoutComponent) iter.next();
      if ((lc.comp instanceof String) && (lc.row > 0)) // its a header, not in first position
        incr++; // leave space by adding a row

      lc.cc.gridY = lc.row + incr + 1; // adjust downward
      lc.ccLabel.gridY = lc.cc.gridY;
      if (debugLayout) System.out.println(lc+" constraint = "+ lc.cc);
    }

    // now create row layout spec
    sbuff.setLength(0);
    int currRow = -1;
    iter = layoutComponents.iterator();
    while (iter.hasNext()) {
      LayoutComponent lc = (LayoutComponent) iter.next();
      while (lc.row > currRow) {
        if ((lc.comp instanceof String) && (lc.row > 0)) {
          sbuff.append( ", 5dlu, default");
        } else if ((lc.comp == null)) {
          sbuff.append(", ").append(lc.col).append("dlu");
        } else {
          if (currRow >= 0) sbuff.append(", ");
          sbuff.append("default");
        }
        currRow++;
      }
    }
    String rowSpec = sbuff.toString();
    if (debugLayout) System.out.println(" row layout = "+ rowSpec);

    // the jgoodies form layout
    FormLayout layout = new FormLayout( colSpec, rowSpec);

    PanelBuilder builder = new PanelBuilder(layout);
    builder.setDefaultDialogBorder();

    CellConstraints cc = new CellConstraints();

    // now add each component with correct constraint
    iter = layoutComponents.iterator();
    while (iter.hasNext()) {
      LayoutComponent lc = (LayoutComponent) iter.next();

      if (lc.comp instanceof Field) {
        Field fld = (Field) lc.comp;
        builder.addLabel( fld.getLabel()+":", lc.ccLabel);
        Component comp = fld.getEditComponent();
        if (lc.comp instanceof Field.TextArea)
          comp= new JScrollPane( comp);
        builder.add( comp,  lc.cc);
      } else if (lc.comp instanceof String) {
         String header = (String) lc.comp;
         builder.addSeparator(header, cc.xyw(1, lc.cc.gridY, ncols));
      }  else if (lc.comp instanceof Component) {
         builder.add( (Component) lc.comp,  lc.cc);
      }
    }

    mainPanel =  builder.getPanel();

      // button panel
    JPanel buttPanel = new JPanel();
    JButton acceptButton = new JButton("Apply");
    buttPanel.add(acceptButton, null);
    for (JComponent auxButton : auxButtons)
      buttPanel.add( auxButton, null);

      // button listeners
    acceptButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        accept();
      }
    });

    setLayout( new BorderLayout());
    add(mainPanel, BorderLayout.CENTER);

    if (addButtons) {
      if (where.equals( BorderLayout.SOUTH)) {
        JPanel south = new JPanel();
        south.setLayout(new BoxLayout( south, BoxLayout.Y_AXIS));
        south.add( new JSeparator(SwingConstants.HORIZONTAL));
        south.add( buttPanel);
        add( south, BorderLayout.SOUTH);
      } else
        add(buttPanel, where);
    }

    finished = true;
  }

  // helper class to use jgoodies form to do layout.
  // Each field gets one of these
  private static class LayoutComponent {
    Object comp;
    int row, col;
    CellConstraints cc, ccLabel;

    LayoutComponent(Object comp, int col, int row, String constraint ) {
      this.comp = comp;
      this.row = row;
      this.col = col;

      this.cc = new CellConstraints( 1+", "+1+" "+(constraint == null ? "" : constraint));
      if (cc.gridWidth > 1)
        cc.gridWidth = cc.gridWidth * 2 - 1;
      this.ccLabel = new CellConstraints();
    }

    public String toString() {
      if (comp == null)
        return "empty row";
      if (comp instanceof Field) return ((Field)comp).getName();
      return comp.getClass().getName();
    }

  }

  /**
   * A convenience class for constructing a standalone JDialog window that has a PrefPanel inside it.
   * To show it on screen, call dialog.show().
   * Example:
   *

   *
    PrefPanel.Dialog d = new PrefPanel.Dialog( frame, true, "testDialogue",
                     (PersistenceManagerExt) store.node("dialog"));
    PrefPanel pp2 = d.getPrefPanel();
    pp2.addHeading("This is Not Your Life:");
    pp2.addTextField("name", "name", "defValue");
    pp2.addTextField("name2", "name2", "defValue22");
    pp2.addTextField("name3", "name3", "defValue22 asd jalskdjalksjd");
    pp2.addSeparator();
    pp2.addHeading("Part Two:");
    pp2.addPasswordField("password", "password", "secret");
    pp2.addIntField("testInt", "testInt", 1234);
    pp2.addDoubleField("testD", "testD", 1234.45);
    pp2.addCheckBoxField("testB", "testB", true);
    pp2.newColumn();
    pp2.addHeading("Another Column:");
    pp2.addDateField("date", "date", new Date());
    try {
      pp2.addTextFormattedField("ff", "ff", new javax.swing.text.MaskFormatter("(###) ###-####"), "(303) 497-1234");
    } catch (java.text.ParseException e) { }
    ArrayList list = new ArrayList(5);
    list.add("this");
    list.add("is");
    list.add("new");
    list.add("but");
    list.add("really too longs");
    pp2.addTextComboField("combo", "combo", list, 5);

    d.finish();
    d.show();

   */
  static public class Dialog extends JDialog {
    private PrefPanel pp;
    private PreferencesExt substore = null;

    /** constructor
       @param parent      JFrame (application) or JApplet (applet)
       @param modal     true is modal (must finish editing before can do anything else)
       @param title       title of window
       @param prefs       PersistenceManagerExt store: keep values in here; may be null.
     */
    public Dialog(RootPaneContainer parent, boolean modal, String title, PreferencesExt prefs) {
      this(parent, modal, title, prefs, prefs);
    }

    /** constructor
       @param parent      JFrame (application) or JApplet (applet)
       @param modal     true is modal (must finish editing before can do anything else)
       @param title       title of window
       @param prefs       PersistenceManagerExt store: keep values in here; may be null.
     */
    public Dialog(RootPaneContainer parent, boolean modal, String title, Preferences prefs, PersistenceManager storeData) {
      super((parent != null) && (parent instanceof JFrame) ? (JFrame) parent : findActiveFrame());
      setModal(modal);
      if (title != null)
        setTitle(title);
      if (prefs != null)
        substore = (PreferencesExt) prefs.node("Dialog");

      if (substore != null) {
        Rectangle r = (Rectangle) substore.getBean("Bounds", null);
        if (r != null) setBounds(r);
      }

      // L&F may change
      UIManager.addPropertyChangeListener( new PropertyChangeListener() {
        public void propertyChange( PropertyChangeEvent e) {
          if (e.getPropertyName().equals("lookAndFeel"))
            SwingUtilities.updateComponentTreeUI( Dialog.this);
        }
      });

      Container cp = getContentPane();
      pp = new PrefPanel( title, prefs, storeData);
      cp.add(pp, BorderLayout.CENTER);

        // add a dismiss button
      JButton dismiss = new JButton("Cancel");
      dismiss.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          setVisible( false);
        }
      });
      pp.addButton( dismiss);

      // watch for accept
      pp.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          setVisible( false);
        }
      });

        // catch move, resize events
      addComponentListener( new ComponentAdapter() {
        public void componentMoved( ComponentEvent e) {
          if (substore != null)
            substore.putBeanObject("Bounds", getBounds());
        }
         public void componentResized( ComponentEvent e) {
          if (substore != null)
            substore.putBeanObject("Bounds", getBounds());
        }
      });

    }

    /** Get the PrefPanel */
    public PrefPanel getPrefPanel() { return pp; }

    /**
     * Find the field with the specified name.
     * @param name of Field
     * @return Field or null if not found
     */
    public Field getField(String name) {
      return pp.getField(name);
    }

    /** Call this when done adding Fields to the prefPanel, instead of calling
     *  pp.finish().
     */
    public void finish() {
      pp.finish();
      pack();

        // persistent state
      if (substore != null) {
        Rectangle b = (Rectangle) substore.getBean("Bounds", null);
        if (b != null)
          setBounds( b);
        substore.putBeanObject("Bounds", getBounds());
      }
    }
  }

  // thanks to Heinz M. Kabutz
  static public Frame findActiveFrame() {
    Frame[] frames = JFrame.getFrames();
    for (Frame frame : frames) {
      if (frame.isVisible())
        return frame;
    }
    return null;
  }

}


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

// $Id: NcSDString.java 51 2006-07-12 17:13:13Z caron $

package thredds.server.opendap;

import opendap.servers.*;

import java.io.IOException;
import java.io.DataOutputStream;

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;

/**
 * Wraps a netcdf scalar or 1D char variable.
 *
 * @author jcaron
 */
public class NcSDString extends SDString implements HasNetcdfVariable {
  private Variable ncVar;
  private String localVal = null;

  /**
   * Constructor
   *
   * @param v : the netcdf Variable
   */
  NcSDString(Variable v) {
      super(Variable.getDAPName(v));
    this.ncVar = v;
  }

  /**
   * Constructor
   *
   * @param name: name of variable
   * @param val:  the value.
   */
  NcSDString(String name, String val) {
    super(name);
    this.localVal = val;
    if (val != null)
      setValue(val);
  }

  /**
   * Read the value (parameters are ignored).
   */
  public boolean read(String datasetName, Object specialO) throws IOException {
    if (localVal == null) // read first time
      setData(ncVar.read());

    setValue(localVal);
    setRead(true);
    return (false);
  }

  public void setData(Array data) {

    if (ncVar.getDataType() == DataType.STRING) {
      localVal = (String) data.getObject(data.getIndex());

    } else { // gotta be a CHAR

      if (ncVar.getRank() == 0) {
        // scalar char - convert to a String
        ArrayChar a = (ArrayChar) data;
        byte[] b = new byte[1];
        b[0] = (byte) a.getChar(0);
        localVal = new String(b, CDM.utf8Charset);
      } else {
        // 1D
        ArrayChar a = (ArrayChar) data;
        localVal = a.getString(a.getIndex()); // fetches the entire String
      }
    }

    setValue(localVal);
    setRead(true);
  }

  public Variable getVariable() { return ncVar; }
  public void serialize(DataOutputStream sink, StructureData sdata, StructureMembers.Member m) throws IOException {
    localVal = sdata.getScalarString(m);
    setValue(localVal);
    externalize( sink);
  }
}


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

package ucar.atd.dorade;

import ucar.nc2.constants.CDM;
import ucar.nc2.util.Misc;

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

class DoradeRADD extends DoradeDescriptor {

  /**
   *
Title: RadarType

   *
Description: nested top level class for defining DORADE radar
   * types

   */
  static class RadarType {
    private String name;

    protected RadarType(String name) {
      this.name = name;
    }

    public String getName() {
      return name;
    }
  }

  /**
   * ground-based radar type
   */
  public static final RadarType TYPE_GROUND = new RadarType("ground");
  /**
   * airborne tail-mounted radar, forward beam type
   */
  public static final RadarType TYPE_AIR_FORE = new RadarType("airborne (fore)");
  /**
   * airborne tail-mounted radar, aft beam type
   */
  public static final RadarType TYPE_AIR_AFT = new RadarType("airborne (aft)");
  /**
   * airborne tail-mounted radar type
   */
  public static final RadarType TYPE_AIR_TAIL = new RadarType("airborne (tail)");
  /**
   * airborne lower fuselage radar type
   */
  public static final RadarType TYPE_AIR_LF = new RadarType("airborne (lower fuselage)");
  /**
   * shipborne radar type
   */
  public static final RadarType TYPE_SHIP = new RadarType("shipborne");
  /**
   * airborne nose radar type
   */
  public static final RadarType TYPE_AIR_NOSE = new RadarType("airborne (nose)");
  /**
   * satellite radar type
   */
  public static final RadarType TYPE_SATELLITE = new RadarType("satellite");
  /**
   * fixed lidar type
   */
  public static final RadarType TYPE_FIXED_LIDAR = new RadarType("fixed lidar");
  /**
   * moving lidar type
   */
  public static final RadarType TYPE_MOVING_LIDAR = new RadarType("moving lidar");

  private static RadarType[] radarTypes = {
          TYPE_GROUND,       // 0
          TYPE_AIR_FORE,     // 1
          TYPE_AIR_AFT,      // 2
          TYPE_AIR_LF,       // 3
          TYPE_AIR_TAIL,     // 4
          TYPE_SHIP,         // 5
          TYPE_AIR_NOSE,     // 6
          TYPE_SATELLITE,    // 7
          TYPE_MOVING_LIDAR, // 8
          TYPE_FIXED_LIDAR,  // 9
  };

  //
  // lookup table mapping DORADE scan mode integer values to ScanMode-s
  //
  private static ScanMode[] scanModeTable = {
          ScanMode.MODE_calibration, // 0
          ScanMode.MODE_PPI,         // 1
          ScanMode.MODE_coplane,     // 2
          ScanMode.MODE_RHI,         // 3
          ScanMode.MODE_vertical,    // 4
          ScanMode.MODE_target,      // 5
          ScanMode.MODE_manual,      // 6
          ScanMode.MODE_idle,        // 7
          ScanMode.MODE_SUR,         // 8
          ScanMode.MODE_air,         // 9
          ScanMode.MODE_horizontal,  // 10
  };

  private String radarName;
  private float radarConstant;
  private float peakPower;
  private float noisePower; // dBm
  private float rcvrGain; // dB
  private float antennaGain; // dB
  private float systemGain; // (antenna gain - waveguide loss) dB
  private float hBeamWidth; // degrees
  private float vBeamWidth; // degrees
  private short radarTypeNdx; // map into radarTypes
  private ScanMode scanMode;
  private float rotVelocity; // requested rotational velocity, deg/s
  private float scanParam0; // mode-specific scan parameter
  private float scanParam1; // mode-specific scan parameter
  private short nParams;
  private short nAdditionalDescriptors;

  //
  // Compression scheme and known legal values
  //
  private short compressionScheme;
  public static final int COMPRESSION_NONE = 0;
  public static final int COMPRESSION_HRD = 1;

  //
  // Data reduction method and known legal values
  //
  private short dataReductionMethod;
  public static final int DATA_REDUCTION_NONE = 1;
  public static final int DATA_REDUCTION_BY_AZIMUTH = 2;
  public static final int DATA_REDUCTION_BY_RANGE = 3;
  public static final int DATA_REDUCTION_BY_ALTITUDE = 4;

  private float reductionBound0; // left azimuth, inner circle diameter, or minimum altitude
  private float reductionBound1; // right azimuth, outer circle diameter, or maximum altitude
  private float longitude;
  private float latitude;
  private float altitude; // km MSL
  private float unambiguousVelocity; // m/s
  private float unambiguousRange; // km
  private short nFrequencies;
  private short nPRTs;
  private float[] frequencies; // GHz
  private float[] PRTs; // ms
  private DoradePARM[] myPARMs;
  private DoradeCELV myCELV;
  private DoradeCFAC myCFAC;

  private int nCells;  // extracted from our CELV

  public DoradeRADD(RandomAccessFile file, boolean littleEndianData) throws DescriptorException {
    byte[] data = readDescriptor(file, littleEndianData, "RADD");

    //
    // unpack
    //
    radarName = new String(data, 8, 8, CDM.utf8Charset).trim();
    radarConstant = grabFloat(data, 16);
    peakPower = grabFloat(data, 20);
    noisePower = grabFloat(data, 24);
    rcvrGain = grabFloat(data, 28);
    antennaGain = grabFloat(data, 32);
    systemGain = grabFloat(data, 36);
    hBeamWidth = grabFloat(data, 40);
    vBeamWidth = grabFloat(data, 44);
    radarTypeNdx = grabShort(data, 48);
    scanMode = scanModeTable[grabShort(data, 50)];
    rotVelocity = grabFloat(data, 52);
    scanParam0 = grabFloat(data, 56);
    scanParam1 = grabFloat(data, 60);
    nParams = grabShort(data, 64);
    nAdditionalDescriptors = grabShort(data, 66);
    compressionScheme = grabShort(data, 68);
    dataReductionMethod = grabShort(data, 70);
    reductionBound0 = grabFloat(data, 72);
    reductionBound1 = grabFloat(data, 76);
    longitude = grabFloat(data, 80);
    latitude = grabFloat(data, 84);
    altitude = grabFloat(data, 88);
    unambiguousVelocity = grabFloat(data, 92);
    unambiguousRange = grabFloat(data, 96);
    nFrequencies = grabShort(data, 100);
    nPRTs = grabShort(data, 102);

    frequencies = new float[5];
    for (int i = 0; i < 5; i++)
      frequencies[i] = grabFloat(data, 104 + 4 * i);

    PRTs = new float[5];
    for (int i = 0; i < 5; i++)
      PRTs[i] = grabFloat(data, 124 + 4 * i);

    //
    // debugging output
    //
    if (verbose)
      System.out.println(this);

    myPARMs = new DoradePARM[nParams];
    for (int i = 0; i < nParams; i++)
      myPARMs[i] = new DoradePARM(file, littleEndianData, this);

    //
    // Find the cell vector (CELV or CSFD)
    //
    try {
      long startpos = file.getFilePointer();
      //  try
      //   {
      String dName = peekName(file);
      switch (dName) {
        case "CELV":
          myCELV = new DoradeCELV(file, littleEndianData);
          break;
        case "CSFD":
          file.seek(startpos);
          myCELV = new DoradeCSFD(file, littleEndianData);
          break;
        default:
          throw new DescriptorException("Expected " + dName + " descriptor not found!");
      }
      //    } catch (DescriptorException ex) {
      //    file.seek(startpos);
      //	myCELV = new DoradeCSFD(file, littleEndianData);
      //   }
    } catch (IOException ioex) {
      throw new DescriptorException(ioex);
    }

    nCells = myCELV.getNCells();

    if (nAdditionalDescriptors > (nParams + 1)) {
      myCFAC = new DoradeCFAC(file, littleEndianData);
    } else {
      myCFAC = null;
    }
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder("DoradeRADD{");
    sb.append("radarName='").append(radarName).append('\'');
    sb.append(", radarConstant=").append(radarConstant);
    sb.append(", peakPower=").append(peakPower);
    sb.append(", noisePower=").append(noisePower);
    sb.append(", rcvrGain=").append(rcvrGain);
    sb.append(", antennaGain=").append(antennaGain);
    sb.append(", systemGain=").append(systemGain);
    sb.append(", hBeamWidth=").append(hBeamWidth);
    sb.append(", vBeamWidth=").append(vBeamWidth);
    sb.append(", radarTypeNdx=").append(radarTypeNdx);
    sb.append(", scanMode=").append(scanMode);
    sb.append(", rotVelocity=").append(rotVelocity);
    sb.append(", scanParam0=").append(scanParam0);
    sb.append(", scanParam1=").append(scanParam1);
    sb.append(", nParams=").append(nParams);
    sb.append(", nAdditionalDescriptors=").append(nAdditionalDescriptors);
    sb.append(", compressionScheme=").append(compressionScheme);
    sb.append(", dataReductionMethod=").append(dataReductionMethod);
    sb.append(", reductionBound0=").append(reductionBound0);
    sb.append(", reductionBound1=").append(reductionBound1);
    sb.append(", longitude=").append(longitude);
    sb.append(", latitude=").append(latitude);
    sb.append(", altitude=").append(altitude);
    sb.append(", unambiguousVelocity=").append(unambiguousVelocity);
    sb.append(", unambiguousRange=").append(unambiguousRange);
    sb.append(", nFrequencies=").append(nFrequencies);
    sb.append(", nPRTs=").append(nPRTs);
    sb.append(", frequencies=").append(Arrays.toString(frequencies));
    sb.append(", PRTs=").append(Arrays.toString(PRTs));
    sb.append(", myPARMs=").append(Arrays.toString(myPARMs));
    sb.append(", myCELV=").append(myCELV);
    sb.append(", myCFAC=").append(myCFAC);
    sb.append(", nCells=").append(nCells);
    sb.append('}');
    return sb.toString();
  }

  /**
   * Get the compression scheme being used.
   *
   * @return the integer defining the compression scheme:
   *

   *
COMPRESSION_NONE
   *
COMPRESSION_HRD
   *

   */
  public int getCompressionScheme() {
    return compressionScheme;
  }

  /**
   * Get the number of cells in a ray.
   *
   * @return the number of cells in a ray
   */
  public int getNCells() {
    return nCells;
  }

  public DoradePARM[] getParamList() {
    return myPARMs;
  }

  public int getNParams() {
    return nParams;
  }

  public String getRadarName() {
    return radarName;
  }

  /**
   * Get radar latitude
   *
   * @return radar latitude in degrees
   */
  public float getLatitude() {
    return latitude;
  }

  /**
   * Get radar longitude
   *
   * @return radar longitude in degrees
   */
  public float getLongitude() {
    return longitude;
  }

  /**
   * Get radar altitude
   *
   * @return radar altitude in km MSL
   */
  public float getAltitude() {
    return altitude;
  }

  /**
   * Get the range to the first cell
   *
   * @return range to the first cell, in meters
   */
  public float getRangeToFirstCell() {
    return myCELV.getCellRanges()[0];
  }

  /**
   * Get the cell spacing.  An exception is thrown if the cell spacing
   * is not constant.
   *
   * @return the cell spacing, in meters
   * @throws DescriptorException if the cell spacing is not constant.
   */
  public float getCellSpacing() throws DescriptorException {
    float[] cellRanges = myCELV.getCellRanges();
    //
    // use the first cell spacing as our expected value
    //
    float cellSpacing = cellRanges[1] - cellRanges[0];
    //
    // Check the rest of the cells against the expected value, allowing
    // 1% fudge
    //
    for (int i = 2; i < cellRanges.length; i++) {
      float space = cellRanges[i] - cellRanges[i - 1];
      if (!Misc.nearlyEquals(space, cellSpacing) && (Math.abs(space / cellSpacing - 1.0) > 0.01)) {
        throw new DescriptorException("variable cell spacing");
      }
    }
    return cellSpacing;
  }

  /**
   * Get the scan mode
   *
   * @return The scan mode
   */
  public ScanMode getScanMode() {
    return scanMode;
  }

  // unidata added
  public float getUnambiguousVelocity() {
    return unambiguousVelocity;
  }

  // unidata added
  public float getunambiguousRange() {
    return unambiguousRange;
  }

  // unidata added
  public float getradarConstant() {
    return radarConstant;
  }

  // unidata added
  public float getrcvrGain() {
    return rcvrGain;
  }

  // unidata added
  public float getantennaGain() {
    return antennaGain;
  }

  // unidata added
  public float getsystemGain() {
    return systemGain;
  }

  // unidata added
  public float gethBeamWidth() {
    return hBeamWidth;
  }

  // unidata added
  public float getpeakPower() {
    return peakPower;
  }

  // unidata added
  public float getnoisePower() {
    return noisePower;
  }
}


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

/**
 * Provides support for the concept of "dimension": pairs of base entities and
 * exponents.
 *
 * @author Steven R. Emmerson
 */
public abstract class Dimension {
    /**
     * The individual elements of this dimension.
     *
     * @serial
     */
    protected final Factor[]       _factors;

    private transient volatile int hashCode;

    /**
     * Constructs a dimensionless dimension from nothing.
     */
    public Dimension() {
        this(new Factor[0]);
    }

    /**
     * Constructs from a single Factor.
     *
     * @param factor
     *            The single Factor that defines the dimension.
     */
    protected Dimension(final Factor factor) {
        this(new Factor[] { factor });
    }

    /**
     * Constructs from an array of Factor-s. This is a trusted constructor for
     * use by subclasses only.
     *
     * @param factors
     *            The factors that define the dimension.
     */
    protected Dimension(final Factor[] factors) {
        _factors = factors;
    }

    /**
     * Returns the rank of this dimension. The rank is the number of base entity
     * and exponent pairs (i.e. the number of Factor-s constituting this
     * dimension).
     *
     * @return The rank of this dimension.
     */
    public final int getRank() {
        return _factors.length;
    }

    /**
     * Returns the array of Factor-s constituting this dimension.
     *
     * @return The array of Factor-s constituting this dimension.
     */
    public final Factor[] getFactors() {
        final Factor[] factors = new Factor[_factors.length];
        System.arraycopy(_factors, 0, factors, 0, factors.length);
        return factors;
    }

    /**
     * Multiplies this dimension by another dimension.
     *
     * @param that
     *            The other dimension.
     * @return The product of the Factor-s of this dimension and the Factor-s of
     *         the other dimension.
     */
    protected Factor[] mult(final Dimension that) {
        // relys on _factors always sorted
        final Factor[] factors1 = _factors;
        final Factor[] factors2 = that._factors;
        int i1 = 0;
        int i2 = 0;
        int k = 0;
        Factor[] newFactors = new Factor[factors1.length + factors2.length];
        for (;;) {
            if (i1 == factors1.length) {
                final int n = factors2.length - i2;
                System.arraycopy(factors2, i2, newFactors, k, n);
                k += n;
                break;
            }
            if (i2 == factors2.length) {
                final int n = factors1.length - i1;
                System.arraycopy(factors1, i1, newFactors, k, n);
                k += n;
                break;
            }
            final Factor f1 = factors1[i1];
            final Factor f2 = factors2[i2];
            final int comp = f1.getID().compareTo(f2.getID());
            if (comp < 0) {
                newFactors[k++] = f1;
                i1++;
            }
            else if (comp == 0) {
                final int exponent = f1.getExponent() + f2.getExponent();
                if (exponent != 0) {
                    newFactors[k++] = new Factor(f1, exponent);
                }
                i1++;
                i2++;
            }
            else {
                newFactors[k++] = f2;
                i2++;
            }
        }
        if (k < newFactors.length) {
            final Factor[] tmp = new Factor[k];
            System.arraycopy(newFactors, 0, tmp, 0, k);
            newFactors = tmp;
        }
        return newFactors;
    }

    /**
     * Raises this dimension to a power.
     *
     * @param power
     *            The power to raise this dimension by.
     * @return The Factor-s of this dimension raised to the power
     *         power.
     */
    protected Factor[] pow(final int power) {
        Factor[] factors;
        if (power == 0) {
            factors = new Factor[0];
        }
        else {
            factors = getFactors();
            if (power != 1) {
                for (int i = factors.length; --i >= 0;) {
                    factors[i] = factors[i].pow(power);
                }
            }
        }
        return factors;
    }

    /**
     * Indicates if this Dimension is the reciprocal of another dimension.
     *
     * @param that
     *            The other dimension.
     * @return true if and only if this dimension is the reciprocal
     *         of the other dimension.
     */
    public final boolean isReciprocalOf(final Dimension that) {
        final Factor[] theseFactors = _factors;
        final Factor[] thoseFactors = that._factors;
        boolean isReciprocalOf;
        if (theseFactors.length != thoseFactors.length) {
            isReciprocalOf = false;
        }
        else {
            int i;
            for (i = theseFactors.length; --i >= 0;) {
                if (!theseFactors[i].isReciprocalOf(thoseFactors[i])) {
                    break;
                }
            }
            isReciprocalOf = i < 0;
        }
        return isReciprocalOf;
    }

    /**
     * Indicates if this dimension is semantically identical to an object.
     *
     * @param object
     *            The object.
     * @return true if and only if this dimension is semantically
     *         identical to object.
     */
    @Override
    public final boolean equals(final Object object) {
        boolean equals;
        if (this == object) {
            equals = true;
        }
        else if (!(object instanceof Dimension)) {
            equals = false;
        }
        else {
            final Factor[] thatFactors = ((Dimension) object)._factors;
            if (_factors.length != thatFactors.length) {
                equals = false;
            }
            else {
                int i = _factors.length;
                while (--i >= 0) {
                    if (!_factors[i].equals(thatFactors[i])) {
                        break;
                    }
                }
                equals = i < 0;
            }
        }
        return equals;
    }

    /**
     * Returns the hash code of this instance.
     *
     * @return The hash code of this instance.
     */
    @Override
    public int hashCode() {
        if (hashCode == 0) {
            int hash = 0;
            for (int i = 0; i < _factors.length; i++) {
                hash ^= _factors[i].hashCode();
            }
            hashCode = hash;
        }
        return hashCode;
    }

    /**
     * Indicates if this dimension is dimensionless. A dimension is
     * dimensionless if it has no Factor-s or if all Factor-s are, themselves,
     * dimensionless.
     *
     * @return true if and only if this dimension is dimensionless.
     */
    public final boolean isDimensionless() {
        for (int i = _factors.length; --i >= 0;) {
            if (!_factors[i].isDimensionless()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the string representation of this dimension.
     *
     * @return The string representation of this dimension.
     */
    @Override
    public String toString() {
        final StringBuilder buf = new StringBuilder(40);
        for (int i = 0; i < _factors.length; i++) {
            buf.append(_factors[i]).append('.');
        }
        if (buf.length() != 0) {
            buf.setLength(buf.length() - 1);
        }
        return buf.toString();
    }
}


package ucar.nc2.ogc.om;

import net.opengis.om.x20.OMObservationPropertyType;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.ft.StationTimeSeriesFeature;

import java.io.IOException;

/**
 * Created by cwardgar on 2014/03/13.
 */
public abstract class NcOMObservationPropertyType {
    // wml2:Collection/wml2:observationMember
    public static OMObservationPropertyType initObservationMember(OMObservationPropertyType observationMember,
            StationTimeSeriesFeature stationFeat, VariableSimpleIF dataVar) throws IOException {
        // om:OM_Observation
        NcOMObservationType.initOmObservation(observationMember.addNewOMObservation(), stationFeat, dataVar);

        return observationMember;
    }

    private NcOMObservationPropertyType() { }
}


/*
 * Copyright 1997, 1998 Sun Microsystems, Inc. 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.
 *
 * - Redistribution 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 Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY
 * DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT OF OR
 * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THIS SOFTWARE OR
 * ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT,
 * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF
 * THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS
 * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 */
package ucar.nc2.ui.table;

import javax.swing.tree.*;
import javax.swing.event.*;

/**
 * @version 1.2 10/27/98
 * An abstract implementation of the TreeTableModel interface, handling the list
 * of listeners.
 * @author Philip Milne
 */

public abstract class TreeTableModelAbstract implements TreeTableModel {
    protected Object root;
    protected EventListenerList listenerList = new EventListenerList();

    public TreeTableModelAbstract(Object root) {
        this.root = root;
    }

    //
    // Default implmentations for methods in the TreeModel interface.
    //

    public Object getRoot() {
        return root;
    }

    public boolean isLeaf(Object node) {
        return getChildCount(node) == 0;
    }

    public void valueForPathChanged(TreePath path, Object newValue) {}

    // This is not called in the JTree's default mode: use a naive implementation.
    public int getIndexOfChild(Object parent, Object child) {
        for (int i = 0; i < getChildCount(parent); i++) {
            if (getChild(parent, i).equals(child)) {
                return i;
            }
        }
        return -1;
    }

    public void addTreeModelListener(TreeModelListener l) {
        listenerList.add(TreeModelListener.class, l);
    }

    public void removeTreeModelListener(TreeModelListener l) {
        listenerList.remove(TreeModelListener.class, l);
    }

    /*
     * Notify all listeners that have registered interest for
     * notification on this event type.  The event instance
     * is lazily created using the parameters passed into
     * the fire method.
     * @see EventListenerList
     */
    protected void fireTreeNodesChanged(Object source, Object[] path,
                                        int[] childIndices,
                                        Object[] children) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==TreeModelListener.class) {
                // Lazily create the event:
                if (e == null)
                    e = new TreeModelEvent(source, path,
                                           childIndices, children);
                ((TreeModelListener)listeners[i+1]).treeNodesChanged(e);
            }
        }
    }

    /*
     * Notify all listeners that have registered interest for
     * notification on this event type.  The event instance
     * is lazily created using the parameters passed into
     * the fire method.
     * @see EventListenerList
     */
    protected void fireTreeNodesInserted(Object source, Object[] path,
                                        int[] childIndices,
                                        Object[] children) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==TreeModelListener.class) {
                // Lazily create the event:
                if (e == null)
                    e = new TreeModelEvent(source, path,
                                           childIndices, children);
                ((TreeModelListener)listeners[i+1]).treeNodesInserted(e);
            }
        }
    }

    /*
     * Notify all listeners that have registered interest for
     * notification on this event type.  The event instance
     * is lazily created using the parameters passed into
     * the fire method.
     * @see EventListenerList
     */
    protected void fireTreeNodesRemoved(Object source, Object[] path,
                                        int[] childIndices,
                                        Object[] children) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        TreeModelEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==TreeModelListener.class) {
                // Lazily create the event:
                if (e == null)
                    e = new TreeModelEvent(source, path,
                                           childIndices, children);
                ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e);
            }
        }
    }

    /*
     * Notify all listeners that have registered interest for
     * notification on this event type.  The event instance
     * is lazily created using the parameters passed into
     * the fire method.
     * @see EventListenerList
     */
    protected void fireTreeStructureChanged(Object source, Object[] path,
          int[] childIndices, Object[] children) {
      //System.out.println("fireTreeStructureChanged");

      // Guaranteed to return a non-null array
      Object[] listeners = listenerList.getListenerList();
      TreeModelEvent e = null;
      // Process the listeners last to first, notifying
      // those that are interested in this event
      for (int i = listeners.length-2; i>=0; i-=2) {
        if (listeners[i]==TreeModelListener.class) {
          // Lazily create the event:
          if (e == null)
              e = new TreeModelEvent(source, path, childIndices, children);
          ((TreeModelListener)listeners[i+1]).treeStructureChanged(e);
        }
      }
    }

    //
    // Default impelmentations for methods in the TreeTableModel interface.
    //

    public Class getColumnClass(int column) { return Object.class; }

   /** By default, make the column with the Tree in it the only editable one.
    *  Making this column editable causes the JTable to forward mouse
    *  and keyboard events in the Tree column to the underlying JTree.
    */
    public boolean isCellEditable(Object node, int column) {
         return getColumnClass(column) == TreeTableModel.class;
    }

    public void setValueAt(Object aValue, Object node, int column) {}

    // Left to be implemented in the subclass:

    /*
     *   public Object getChild(Object parent, int index)
     *   public int getChildCount(Object parent)
     *   public int getColumnCount()
     *   public String getColumnName(Object node, int column)
     *   public Object getValueAt(Object node, int column)
     */
}


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

package ucar.nc2.ncml;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jdom2.Element;
import thredds.client.catalog.Catalog;
import thredds.inventory.MFile;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.Group;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Variable;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.dataset.DatasetConstructor;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.util.CancelTask;

/**
 * JoinExisting Aggregation.
 *
 * @author caron
 */
public class AggregationExisting extends AggregationOuterDimension {

  public AggregationExisting(NetcdfDataset ncd, String dimName, String recheckS) {
    super(ncd, dimName, Aggregation.Type.joinExisting, recheckS);
  }

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

    // LOOK not dealing with groups

    // a little tricky to get the coord var cached if we have to read through the datasets on the buildCoords()
    String dimName = getDimensionName();
    CacheVar coordCacheVar = null;
    if (type != Aggregation.Type.joinExistingOne) {
      Variable tcv = typical.findVariable(dimName);
      if (tcv != null) {
        coordCacheVar = new CoordValueVar(dimName, tcv.getDataType(), tcv.getUnitsString());
      } else {
        // LOOK what to do; docs claim we can add the coord variable in the outer ncml, so make a fake one for now
        VariableDS fake = new VariableDS(ncDataset, null, null, dimName, DataType.INT, dimName, null, null);
        // fake.setValues(npts, 0, 1);
        ncDataset.addVariable(null, fake);
      }

    } else {
      coordCacheVar = new CoordValueVar(dimName, DataType.STRING, "");
    }
    if (coordCacheVar != null)
      cacheList.add(coordCacheVar);  // coordinate variable is always cached

    // gotta check persistence info - before buildCoords - if its going to do any good
    persistRead();

    // now find out how many coordinates we have, caching values if needed
    buildCoords(cancelTask);

    // create aggregation dimension, now that we know the size
    Dimension aggDim = new Dimension(dimName, getTotalCoords());
    ncDataset.removeDimension(null, dimName); // remove previous declaration, if any
    ncDataset.addDimension(null, aggDim);

    promoteGlobalAttributes((DatasetOuterDimension) typicalDataset);

    // now create the agg variables
    // all variables with the named aggregation dimension
    for (Variable v : typical.getVariables()) {
      if (v.getRank() < 1)
        continue;
      String outerName = v.getDimension(0).makeFullName();
      if (!dimName.equals(outerName))
        continue;

      Group newGroup = DatasetConstructor.findGroup(ncDataset, v.getParentGroup());
      VariableDS vagg = new VariableDS(ncDataset, newGroup, null, v.getShortName(), v.getDataType(),
              v.getDimensionsString(), null, null);
      vagg.setProxyReader(this);
      DatasetConstructor.transferVariableAttributes(v, vagg);

      newGroup.removeVariable(v.getShortName());
      newGroup.addVariable(vagg);
      aggVars.add(vagg);

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

    // handle the agg coordinate variable
    VariableDS joinAggCoord = (VariableDS) ncDataset.findVariable(dimName); // long name of dimension, coord variable
    if ((joinAggCoord == null) && (type == Type.joinExisting)) {
      typicalDataset.close(typical); // clean up
      throw new IllegalArgumentException("No existing coordinate variable for joinExisting on " + getLocation());
    }

    if (type == Type.joinExistingOne) {
      if (joinAggCoord != null) {
        // replace aggregation coordinate variable
        ncDataset.getRootGroup().removeVariable(joinAggCoord.getShortName());
      }

      joinAggCoord = new VariableDS(ncDataset, null, null, dimName, DataType.STRING, dimName, null, null);
      joinAggCoord.setProxyReader(this);
      ncDataset.getRootGroup().addVariable(joinAggCoord);
      aggVars.add(joinAggCoord);

      joinAggCoord.addAttribute(new ucar.nc2.Attribute(_Coordinate.AxisType, "Time"));
      joinAggCoord.addAttribute(new Attribute(CDM.LONG_NAME, "time coordinate"));
      joinAggCoord.addAttribute(new ucar.nc2.Attribute(CF.STANDARD_NAME, "time"));
    }

    if (timeUnitsChange && joinAggCoord != null) {
      readTimeCoordinates(joinAggCoord, cancelTask);
    }

    // make it a cacheVar
    if (joinAggCoord != null)
      joinAggCoord.setSPobject(coordCacheVar);

    // check persistence info - may have cached values other than coordinate  LOOK ????
    persistRead();

    setDatasetAcquireProxy(typicalDataset, ncDataset);
    typicalDataset.close(typical);

    if (debugInvocation)
      System.out.println(ncDataset.getLocation() + " invocation count = " + AggregationOuterDimension.invocation);

    ncDataset.finish();
  }

  /**
   * Persist info (ncoords, coordValues) from joinExisting, since that can be expensive to recreate.
   *
   * @throws IOException
   */
  public void persistWrite() throws IOException {
    if (diskCache2 == null)
      return;

    String cacheName = getCacheName();
    if (cacheName == null) return;
    if (cacheName.startsWith("file:"))      // LOOK
      cacheName = cacheName.substring(5);
    File cacheFile = diskCache2.getCacheFile(cacheName);
    if (cacheFile == null) throw new IllegalStateException();

    // only write out if something changed after the cache file was last written, or if the file has been deleted
    if (!cacheDirty && cacheFile.exists())
      return;

    FileChannel channel = null;
    try {
      File dir = cacheFile.getParentFile();
      if (!dir.exists()) {
        if (!dir.mkdirs())
          logger.error("Cant make cache directory= " + cacheFile);
      }

      // Get a file channel for the file
      FileOutputStream fos = new FileOutputStream(cacheFile);
      channel = fos.getChannel();

      // Try acquiring the lock without blocking. This method returns
      // null or throws an exception if the file is already locked.
      FileLock lock;
      try {
        lock = channel.tryLock();
      } catch (OverlappingFileLockException e) {
        // File is already locked in this thread or virtual machine
        return; // give up
      }
      if (lock == null) return;

      PrintWriter out = new PrintWriter(new OutputStreamWriter(fos, CDM.utf8Charset));
      out.print("<?xml version='1.0' encoding='UTF-8'?>\n");
      out.print("<aggregation xmlns='http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2' version='3' ");
      out.print("type='" + type + "' ");
      if (dimName != null)
        out.print("dimName='" + dimName + "' ");
      if (datasetManager.getRecheck() != null)
        out.print("recheckEvery='" + datasetManager.getRecheck() + "' ");
      out.print(">\n");

      List<Dataset> nestedDatasets = getDatasets();
      for (Dataset dataset : nestedDatasets) {
        DatasetOuterDimension dod = (DatasetOuterDimension) dataset;
        if (dod.getId() == null) logger.warn("id is null");

        out.print("  <netcdf id='" + dod.getId() + "' ");
        out.print("ncoords='" + dod.getNcoords(null) + "' >\n");

        for (CacheVar pv : cacheList) {
          Array data = pv.getData(dod.getId());
          if (data != null) {
            out.print("    <cache varName='" + pv.varName + "' >");
            while (data.hasNext())
              out.printf("%s ", data.next());
            out.print("</cache>\n");
            if (logger.isDebugEnabled())
              logger.debug(" wrote array = " + pv.varName + " nelems= " + data.getSize() + " for " + dataset.getLocation());
          }
        }
        out.print("  </netcdf>\n");
      }

      out.print("</aggregation>\n");
      out.close(); // this also closes the  channel and releases the lock

      long time = datasetManager.getLastScanned();
      if (time == 0) time = System.currentTimeMillis(); // no scans (eg all static) will have a 0

      if (!cacheFile.setLastModified(time))
        logger.warn("FAIL to set lastModified on {}", cacheFile.getPath());
      cacheDirty = false;

      if (logger.isDebugEnabled())
        logger.debug("Aggregation persisted = " + cacheFile.getPath() + " lastModified= " + new Date(datasetManager.getLastScanned()));

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

  // read info from the persistent XML file, if it exists
  protected void persistRead() {
    if (diskCache2 == null) return;

    String cacheName = getCacheName();
    if (cacheName == null) return;
    if (cacheName.startsWith("file:"))      // LOOK
      cacheName = cacheName.substring(5);

    File cacheFile = diskCache2.getCacheFile(cacheName);
    if (cacheFile == null) throw new IllegalStateException();
    if (!cacheFile.exists())
      return;
    long lastWritten = cacheFile.lastModified();

    if (logger.isDebugEnabled()) logger.debug(" Try to Read cache {} " + cacheFile.getPath());

    Element aggElem;
    try {
      aggElem = ucar.nc2.util.xml.Parse.readRootElement("file:" + cacheFile.getPath());
    } catch (IOException e) {
      if (debugCache) System.out.println(" No cache for " + cacheName + " - " + e.getMessage());
      return;
    }

    String version = aggElem.getAttributeValue("version");
    if ((version == null) || !version.equals("3")) return; // dont read old cache files, recreate

    // use a map to find datasets to avoid O(n**2) searching
    Map<String, Dataset> map = new HashMap<String, Dataset>();
    for (Dataset ds : getDatasets()) {
      map.put(ds.getId(), ds);
    }

    List<Element> ncList = aggElem.getChildren("netcdf", Catalog.ncmlNS);
    for (Element netcdfElemNested : ncList) {
      String id = netcdfElemNested.getAttributeValue("id");
      DatasetOuterDimension dod = (DatasetOuterDimension) map.get(id);

      if (null == dod) {
        // this should mean that the dataset has been deleted. so not a problem
        if (logger.isDebugEnabled()) logger.debug(" have cache but no dataset= {}", id);
        continue;
      }
      if (logger.isDebugEnabled()) logger.debug(" use cache for dataset= {}", id);

      MFile mfile = dod.getMFile();
      if (mfile != null && mfile.getLastModified() > lastWritten) {  // skip datasets that have changed
        if (logger.isDebugEnabled()) logger.debug(" dataset was changed= {}", mfile);
        continue;
      }

      if (dod.ncoord == 0) {
        String ncoordsS = netcdfElemNested.getAttributeValue("ncoords");
        try {
          dod.ncoord = Integer.parseInt(ncoordsS);
          if (logger.isDebugEnabled()) logger.debug(" Read the cache; ncoords = {}", dod.ncoord);
        } catch (NumberFormatException e) {
          logger.error("bad ncoord attribute on dataset=" + id);
        }
      }

      // if (dod.coordValue != null) continue; // allow ncml to override

      List<Element> cacheElemList = netcdfElemNested.getChildren("cache", Catalog.ncmlNS);
      for (Element cacheElemNested : cacheElemList) {
        String varName = cacheElemNested.getAttributeValue("varName");
        CacheVar pv = findCacheVariable(varName);
        if (pv != null) {
          String sdata = cacheElemNested.getText();
          if (sdata.length() == 0) continue;
          if (logger.isDebugEnabled()) logger.debug(" read data for var = " + varName + " size= " + sdata.length());

          //long start = System.nanoTime();
          String[] vals = sdata.split(" ");
          //double took = .001 * .001 * .001 * (System.nanoTime() - start);
          //if (debugPersist) System.out.println("  split took = " + took + " sec; ");

          try {
            //start = System.nanoTime();
            Array data = Array.makeArray(pv.dtype, vals);
            //took = .001 * .001 * .001 * (System.nanoTime() - start);
            //if (debugPersist) System.out.println("  makeArray took = " + took + " sec nelems= "+data.getSize());
            pv.putData(id, data);
            countCacheUse++;

          } catch (Exception e) {
            logger.warn("Error reading cached data ", e);
          }

        } else {
          logger.warn("not a cache var=" + varName);
        }
      }
    }

  }

// name to use in the DiskCache2 for the persistent XML info.
// Document root is aggregation

  // has the name getCacheName()
  private String getCacheName() {
    String cacheName = ncDataset.getLocation();
    if (cacheName == null) cacheName = ncDataset.getCacheName();
    return cacheName;
  }

  //////////////////////////////////////////////////
  // back door for testing
  static public int countCacheUse = 0;

}


/*
 * 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 ucar.nc2.constants.CDM;
import ucar.nc2.grib.GribResourceReader;
import ucar.nc2.grib.grib1.Grib1Parameter;
import ucar.nc2.grib.grib1.Grib1Record;
import ucar.nc2.grib.grib1.Grib1SectionProductDefinition;

import javax.annotation.concurrent.Immutable;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * This is the interface to manage GRIB-1 Parameter Table lookups.
 * A lookup is a (center, subcenter, version) --> Parameter Table path.
 * The lookups are loaded at startup, but the Parameter Tables arent read until requested,
 *   via getParameter(int center, int subcenter, int tableVersion, int param_number).
 *
 * These are the tables that are loaded at runtime, matching center and versions.
 *

 * Allow different table versions in the same file.
 * Allow overriding standard grib1 tables on the dataset level.
 *
 * @author caron
 * @since 9/13/11
 */
@Immutable
public class Grib1ParamTables {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Grib1ParamTables.class);

  private static final Object lock = new Object();
  private static int standardTablesStart = 0; // heres where the standard tables start - keep track so user additions can go first

  private static Lookup standardLookup;
  private static Grib1ParamTableReader defaultWmoTable;

  static {
    try {
      standardLookup = new Lookup();
      standardLookup.readLookupTable("resources/grib1/lookupTables.txt");
      standardLookup.readLookupTable("resources/grib1/ecmwfEcCodes/lookupTables.txt");
      standardLookup.readLookupTable("resources/grib1/ecmwf/lookupTables.txt");
      standardLookup.readLookupTable("resources/grib1/ncl/lookupTables.txt");
      standardLookup.readLookupTable("resources/grib1/dss/lookupTables.txt");
      // standardLookup.readLookupTable("resources/grib1/ncep/lookupTables.txt");
      standardLookup.readLookupTable("resources/grib1/wrf/lookupTables.txt"); // */
      // lookup.readLookupTable("resources/grib1/tablesOld/lookupTables.txt");  // too many problems - must check every one !
      standardLookup.tables = new CopyOnWriteArrayList<>(standardLookup.tables); // in case user adds tables
      defaultWmoTable = standardLookup.getParameterTable(0, -1, -1); // user cannot override default

    } catch (Throwable t) {
      logger.warn("Grib1ParamTables init failed: ", t);
    }
  }

  private static boolean strict = false;

  public static boolean isStrict() {
    return strict;
  }

  /**
   * Set strict mode.
   *
If strict:
   *
Must find a match in the tables. Otherwise, use default

   *
Tables cannot override standard WMO parameters. Thus param_no < 128 and version < 128 must use default table

   *

   *
   * @param strict true for strict mode.
   */
  public static void setStrict(boolean strict) {
    Grib1ParamTables.strict = strict;
  }

  public static Grib1ParamTableReader getDefaultWmoTable() {
    return defaultWmoTable;
  }

  // Make a key from (center, subcenter, version) that provides correct sort order.
  static int makeKey(int center, int subcenter, int version) {
    if (center < 0) center = 255;
    if (subcenter < 0) subcenter = 255;
    if (version < 0) version = 255;
    return center * 1000 * 1000 + subcenter * 1000 + version;
  }

  static String showKey(int key) {
    int center = key/(1000*1000);
    key = key - center * (1000*1000);
    int subcenter = key / (1000);
    key = key - subcenter * 1000;
    int version = key;
    return String.format("%2d-%2d-%2d", center, subcenter, version);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // Map of paramTablePath -> Grib1ParamTableReader
  private static final Map<String, Grib1ParamTableReader> localTableHash = new ConcurrentHashMap<>();

  /**
   * Get a Grib1ParamTables object, optionally specifying a parameter table or lookup table specific to this dataset.
   *
   * @param paramTablePath  path to a parameter table, in format Grib1ParamTable can read.
   * @param lookupTablePath path to a lookup table, in format Lookup.readLookupTable() can read.
   * @return Grib1Tables
   * @throws IOException on read error
   */
  public static Grib1ParamTables factory(String paramTablePath, String lookupTablePath) throws IOException {
    if (paramTablePath == null && lookupTablePath == null) return new Grib1ParamTables();
    Lookup lookup = null;
    Grib1ParamTableReader override = null;

    Grib1ParamTableReader table;
    if (paramTablePath != null) {
      table = localTableHash.get(paramTablePath);
      if (table == null) {
        table = new Grib1ParamTableReader(paramTablePath);
        localTableHash.put(paramTablePath, table);
        override = table;
      }
    }

    if (lookupTablePath != null) {
      lookup = new Lookup();
      if (!lookup.readLookupTable(lookupTablePath))
        throw new FileNotFoundException("cant read lookup table=" + lookupTablePath);
    }

    return new Grib1ParamTables(lookup, override);
  }

  /**
   * Get a Grib1Tables object, optionally specifiying a parameter table in XML specific to this dataset.
   *
   * @param paramTableElem parameter table in XML
   * @return Grib1Tables
   */
  public static Grib1ParamTables factory(org.jdom2.Element paramTableElem) {
    if (paramTableElem == null) return new Grib1ParamTables();
    return new Grib1ParamTables(null, new Grib1ParamTableReader(paramTableElem));
  }

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

  private final Lookup lookup; // if lookup table was set
  private final Grib1ParamTableReader override; // Dataset specific override.

  // This is the "StandardLookup". LOOK rename Grib1ParamTables -> Grib1ParamLookup.
  public Grib1ParamTables() {
    this.lookup = null;
    this.override = null;
  }

  // Possible overrides of the StandardLookup.
  private Grib1ParamTables(Lookup lookup, Grib1ParamTableReader override) {
    this.lookup = lookup;
    this.override = override;
  }

  public Grib1Parameter getParameter(Grib1Record record) {
    Grib1SectionProductDefinition pds = record.getPDSsection();
    return getParameter(pds.getCenter(), pds.getSubCenter(), pds.getTableVersion(), pds.getParameterNumber());
  }

  public Grib1Parameter getParameter(int center, int subcenter, int tableVersion, int param_number) {
    Grib1Parameter param = null;
    if (override != null)
      param = override.getParameter(param_number);
    if (param == null && lookup != null)
      param = lookup.getParameter(center, subcenter, tableVersion, param_number);
    if (param == null)
      param = standardLookup.getParameter(center, subcenter, tableVersion, param_number);
    return param;
  }

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

  /**
   * Debugging only
   */
  public Grib1ParamTableReader getParameterTable(int center, int subcenter, int tableVersion) {
    Grib1ParamTableReader result = null;
    if (lookup != null)
      result = lookup.getParameterTable(center, subcenter, tableVersion);
    if (result == null)
      result = standardLookup.getParameterTable(center, subcenter, tableVersion); // standard tables

    return result;
  }

  // debugging
  public static List<Grib1ParamTableReader> getStandardParameterTables() {
    return standardLookup.tables;
  }

  /**
   * Add all tables in list to standard tables
   *
   * @param lookupFilename filename containing list of tables
   * @return true if  read ok, false if file not found
   * @throws IOException if file found but read error
   */
  public static boolean addParameterTableLookup(String lookupFilename) throws IOException {
    Lookup lookup = new Lookup();
    if (!lookup.readLookupTable(lookupFilename))
      return false;

    synchronized (lock) {
      standardLookup.tables.addAll(standardTablesStart, lookup.tables);
      standardTablesStart += lookup.tables.size();
    }
    return true;
  }

  /**
   * Add table to standard tables for a specific center, subcenter and version.
   *
   * @param center        center id
   * @param subcenter     subcenter id, or -1 for all
   * @param tableVersion  table verssion, or -1 for all
   * @param tableFilename file to read parameter table from
   */
  public static void addParameterTable(int center, int subcenter, int tableVersion, String tableFilename) {
    Grib1ParamTableReader table = new Grib1ParamTableReader(center, subcenter, tableVersion, tableFilename);
    synchronized (lock) {
      standardLookup.tables.add(standardTablesStart, table);
      standardTablesStart++;
    }
  }

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

  private static class Lookup {
    List<Grib1ParamTableReader> tables = new ArrayList<>();
    // Map (center, subcenter, version) -> Grib1ParamTable
    Map<Integer, Grib1ParamTableReader> tableMap = new ConcurrentHashMap<>();

    /**
     * read the lookup table from file
     *
     * @param resourceName read from file
     * @return true if successful
     * @throws IOException On badness
     */
    boolean readLookupTable(String resourceName) throws IOException {
      try (InputStream inputStream = GribResourceReader.getInputStream(resourceName)) {
        return readLookupTable(inputStream, resourceName);
      }
    }

    /**
     * read the lookup table from an input stream
     *
     * @param is         The input stream
     * @param lookupFile full pathname of lookup file
     * @return true if successful
     * @throws IOException On badness
     */
    private boolean readLookupTable(InputStream is, String lookupFile) throws IOException {
      if (is == null)
        return false;

      File parent = new File(lookupFile).getParentFile();
      try (InputStreamReader isr = new InputStreamReader(is, CDM.utf8Charset);
          BufferedReader br = new BufferedReader(isr)) {

        String line;
        while ((line = br.readLine()) != null) {
          line = line.trim();
          if ((line.length() == 0) || line.startsWith("#")) {
            continue;
          }
          String[] tableDefArr = line.split(":");

          int center = Integer.parseInt(tableDefArr[0].trim());
          int subcenter = Integer.parseInt(tableDefArr[1].trim());
          int version = Integer.parseInt(tableDefArr[2].trim());
          String filename = tableDefArr[3].trim();
          String path;
          if (filename.startsWith("/") || filename.startsWith("\\") || filename.startsWith("file:")
              || filename.startsWith("http://")) {
            path = filename;
          } else {
            File tableFile = new File(parent, filename); // reletive file
            path = tableFile.getPath();
          }

          Grib1ParamTableReader table = new Grib1ParamTableReader(center, subcenter, version, path);
          tables.add(table);
        }
      }
      return true;
    }

    private Grib1Parameter getParameter(int center, int subcenter, int tableVersion, int param_number) {
      if (strict && param_number < 128 && tableVersion < 128)
        return defaultWmoTable.getParameter(param_number);

      Grib1ParamTableReader pt = getParameterTable(center, subcenter, tableVersion);
      Grib1Parameter param = null;
      if (pt != null) param = pt.getParameter(param_number);
      if (!strict && param == null) param = defaultWmoTable.getParameter(param_number);
      return param;
    }

    private Grib1ParamTableReader getParameterTable(int center, int subcenter, int tableVersion) {
      // look in hash table
      int key = makeKey(center, subcenter, tableVersion);
      Grib1ParamTableReader table = tableMap.get(key);
      if (table != null)
        return table;

      // match from lookup tables(s)
      table = findParameterTableExact(center, subcenter, tableVersion);
      if (table == null)
        table = findParameterTable(center, subcenter, tableVersion);
      if (table == null) {
        if (strict || defaultWmoTable == null) {
          // table = findParameterTable(center, subcenter, tableVersion); // debug
          logger.warn("Could not find a table for GRIB file with center: " + center + " subCenter: " + subcenter + " version: " + tableVersion);
          throw new UnsupportedOperationException("Could not find a table for GRIB file with center: " + center + " subCenter: " + subcenter + " version: " + tableVersion);
        }
        return defaultWmoTable;
      }

      Grib1ParamTableReader prevTable = tableMap.get(key);
      if (prevTable != null) {
        logger.warn("***Duplicate Table for %s%n   %s%n   %s%n", prevTable.getPath(), table.getPath());
      }

      tableMap.put(key, table); // assume we would get the same table in any thread, so race condition is ok
      return table;
    }

    @Nullable
    private Grib1ParamTableReader findParameterTableExact(int center, int subcenter, int version) {
      List<Grib1ParamTableReader> localCopy = tables; // thread safe
      for (Grib1ParamTableReader table : localCopy) {
        // look for a match
        if (center == table.getCenter_id() && subcenter == table.getSubcenter_id() && version == table.getVersion()) {  // match
          if (table.getParameters() == null) //  see if the parameters for this table have been read in yet.
            continue; // failed - maybe theres another entry table that matches

          // success - initialize other tables with the same path
          for (Grib1ParamTableReader table2 : localCopy) {
            if (table2.getPath().equals(table.getPath()))
              table2.setParameters(table.getParameters());
          }
          return table;
        }
      }
      return null;
    }

    // wildcard match
    @Nullable
    private Grib1ParamTableReader findParameterTable(int center, int subcenter, int version) {
      List<Grib1ParamTableReader> localCopy = tables; // thread safe
      for (Grib1ParamTableReader table : localCopy) {
        // look for a match
        if (center == table.getCenter_id()) {
          if ((table.getSubcenter_id() == -1) || (subcenter == table.getSubcenter_id())) {
            //if ((table.subcenter_id == -1) || (table.subcenter_id == 0) || (subcenter == table.subcenter_id)) {
            if ((table.getVersion() == -1) || version == table.getVersion()) {  // match
              //  see if the parameters for this table have been read in yet.
              if (table.getParameters() == null)
                continue; // failed - maybe theres another entry table that matches

              // success - initialize other tables with the same path
              for (Grib1ParamTableReader table2 : localCopy) {
                if (table2.getPath().equals(table.getPath()))
                  table2.setParameters(table.getParameters());
              }
              return table;
            }
          }
        }
      }
      return null;
    }

  }  // Lookup

}