/*
 * #%L
 * BSD implementations of Bio-Formats readers and writers
 * %%
 * Copyright (C) 2005 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package loci.formats.gui;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import loci.formats.cache.ByteArraySource;
import loci.formats.cache.Cache;
import loci.formats.cache.CacheEvent;
import loci.formats.cache.CacheException;
import loci.formats.cache.CacheListener;
import loci.formats.cache.CrosshairStrategy;
import loci.formats.cache.ICacheSource;
import loci.formats.cache.ICacheStrategy;
import loci.formats.cache.RectangleStrategy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

/**
 * GUI component for managing a cache.
 */
public class CacheComponent extends JPanel
  implements ActionListener, CacheListener, ChangeListener
{

  // -- Constants --

  private static final Logger LOGGER =
    LoggerFactory.getLogger(CacheComponent.class);

  protected static final String[] SOURCES = {"Byte arrays", "BufferedImages"};
  protected static final Class[] SOURCE_VALUES = {
    ByteArraySource.class,
    BufferedImageSource.class
  };
  protected static final Class[] SOURCE_PARAMS = {String.class};
  protected static final String[] STRATEGIES = {"Crosshair", "Rectangle"};
  protected static final Class[] STRATEGY_VALUES = {
    CrosshairStrategy.class,
    RectangleStrategy.class
  };
  protected static final Class[] STRATEGY_PARAMS = {int[].class};

  protected static final String[] PRIORITIES =
    {"Maximum", "High", "Normal", "Low", "Minimum"};
  protected static final int[] PRIORITY_VALUES = {
    ICacheStrategy.MAX_PRIORITY, ICacheStrategy.HIGH_PRIORITY,
    ICacheStrategy.NORMAL_PRIORITY, ICacheStrategy.LOW_PRIORITY,
    ICacheStrategy.MIN_PRIORITY
  };
  protected static final String[] ORDERS = {"Centered", "Forward", "Backward"};
  protected static final int[] ORDER_VALUES = {
    ICacheStrategy.CENTERED_ORDER,
    ICacheStrategy.FORWARD_ORDER,
    ICacheStrategy.BACKWARD_ORDER
  };

  // -- Fields --

  /** The cache that this component controls. */
  private Cache cache;

  /** Combo box for choosing cache source. */
  private JComboBox sourceChooser;

  /** Combo box for choosing cache strategy. */
  private JComboBox strategyChooser;

  /** Spinners for choosing range of slices to cache. */
  private JSpinner[] range;

  /** Combo boxes for choosing axis priority. */
  private JComboBox[] priority;

  /** Combo boxes for choosing planar ordering. */
  private JComboBox[] order;

  /** File name that the cache is working with (debugging only). */
  private String id;

  /** Length of each dimensional axis, obtained from cache strategy. */
  private int[] lengths;

  // -- Constructors --

  /** Creates a cache GUI component. */
  public CacheComponent(Cache cache, String[] axisLabels) {
    this(cache, axisLabels, null);
  }

  /**
   * Creates a cache GUI component with the ability to change between the
   * various source types (mainly for debugging purposes).
   */
  public CacheComponent(Cache cache, String[] axisLabels, String id) {
    super();
    this.cache = cache;
    this.id = id;
    lengths = cache.getStrategy().getLengths();

    setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

    CellConstraints cc = new CellConstraints();

    JPanel top = new JPanel();
    FormLayout layout = new FormLayout("pref,3dlu,pref:grow",
      id == null ? "pref:grow" : "pref:grow,3dlu,pref:grow");
    top.setLayout(layout);

    int col = 1, row = 1;

    // add source choices, if desired
    if (id != null) {
      JLabel label = new JLabel("Objects to cache: ");
      sourceChooser = new JComboBox(SOURCES);
      sourceChooser.setSelectedIndex(sourceIndex(cache.getSource()));
      sourceChooser.setActionCommand("source");
      sourceChooser.addActionListener(this);

      col = 1;
      top.add(label, cc.xy(col, row));
      col += 2;
      top.add(sourceChooser, cc.xy(col, row));
      row += 2;
    }

    ICacheStrategy strategy = cache.getStrategy();

    // add strategy choices
    JLabel label = new JLabel("Caching strategy: ");
    strategyChooser = new JComboBox(STRATEGIES);
    strategyChooser.setSelectedIndex(strategyIndex(strategy));
    strategyChooser.setActionCommand("strategy");
    strategyChooser.addActionListener(this);

    col = 1;
    top.add(label, cc.xy(col, row));
    col += 2;
    top.add(strategyChooser, cc.xy(col, row));
    row += 2;

    JPanel bottom = new JPanel();
    final StringBuilder rows = new StringBuilder();
    rows.append("pref:grow");
    for (int i=0; i<axisLabels.length; i++) rows.append(",3dlu,pref:grow");
    layout = new FormLayout(
      "pref:grow,3dlu,pref:grow,3dlu,pref:grow,3dlu,pref:grow",
      rows.toString());
    bottom.setLayout(layout);

    // add strategy parameter choices
    col = row = 1;
    bottom.add(new JLabel("Axis"), cc.xy(col, row));
    col += 2;
    bottom.add(new JLabel("Range"), cc.xy(col, row));
    col += 2;
    bottom.add(new JLabel("Priority"), cc.xy(col, row));
    col += 2;
    bottom.add(new JLabel("Order"), cc.xy(col, row));
    row += 2;

    range = new JSpinner[lengths.length];
    priority = new JComboBox[lengths.length];
    order = new JComboBox[lengths.length];

    int[] rng = strategy.getRange();
    int[] prio = strategy.getPriorities();
    int[] ord = strategy.getOrder();
    for (int i=0; i<axisLabels.length; i++) {
      JLabel l = new JLabel(axisLabels[i]);
      range[i] = new JSpinner(new SpinnerNumberModel(rng[i], 0, lengths[i], 1));
      priority[i] = new JComboBox(PRIORITIES);
      priority[i].setSelectedIndex(priorityIndex(prio[i]));
      order[i] = new JComboBox(ORDERS);
      order[i].setSelectedIndex(orderIndex(ord[i]));

      col = 1;
      bottom.add(l, cc.xy(col, row));
      col += 2;
      bottom.add(range[i], cc.xy(col, row));
      col += 2;
      bottom.add(priority[i], cc.xy(col, row));
      col += 2;
      bottom.add(order[i], cc.xy(col, row));
      row += 2;

      range[i].addChangeListener(this);
      priority[i].addActionListener(this);
      order[i].addActionListener(this);
    }

    add(top);
    add(Box.createVerticalStrut(9));
    add(bottom);

    cache.addCacheListener(this);
  }

  // -- CacheComponent API methods --

  public Cache getCache() { return cache; }

  public void dispose() {
    cache.removeCacheListener(this);
  }

  // -- ActionListener API methods --

  /** Handles combo box changes. */
  @Override
  public void actionPerformed(ActionEvent e) {
    String cmd = e.getActionCommand();
    if ("source".equals(cmd)) updateSource();
    else if ("strategy".equals(cmd)) updateStrategy();
    else { // priority or order change
      Object src = e.getSource();
      for (int i=0; i<priority.length; i++) {
        if (src == priority[i]) {
          updatePriority(i);
          return;
        }
      }
      for (int i=0; i<order.length; i++) {
        if (src == order[i]) {
          updateOrder(i);
          return;
        }
      }
    }
  }

  // -- CacheListener API methods --

  /** Updates GUI to match latest cache state. */
  @Override
  public void cacheUpdated(CacheEvent e) {
    int type = e.getType();
    ICacheStrategy strategy = cache.getStrategy();
    switch (type) {
      case CacheEvent.SOURCE_CHANGED:
        sourceChooser.removeActionListener(this);
        sourceChooser.setSelectedIndex(sourceIndex(cache.getSource()));
        sourceChooser.addActionListener(this);
        break;
      case CacheEvent.STRATEGY_CHANGED:
        strategyChooser.removeActionListener(this);
        strategyChooser.setSelectedIndex(strategyIndex(strategy));
        strategyChooser.addActionListener(this);
        break;
      case CacheEvent.PRIORITIES_CHANGED:
        int[] prio = strategy.getPriorities();
        for (int i=0; i<prio.length; i++) {
          priority[i].removeActionListener(this);
          priority[i].setSelectedIndex(priorityIndex(prio[i]));
          priority[i].addActionListener(this);
        }
        break;
      case CacheEvent.ORDER_CHANGED:
        int[] ord = strategy.getOrder();
        for (int i=0; i<ord.length; i++) {
          order[i].removeActionListener(this);
          order[i].setSelectedIndex(orderIndex(ord[i]));
          order[i].addActionListener(this);
        }
        break;
      case CacheEvent.RANGE_CHANGED:
        int[] rng = strategy.getRange();
        for (int i=0; i<rng.length; i++) {
          range[i].removeChangeListener(this);
          range[i].setValue(rng[i]);
          range[i].addChangeListener(this);
        }
        break;
    }
  }

  // -- ChangeListener API methods --

  /** Handles range spinner changes. */
  @Override
  public void stateChanged(ChangeEvent e) {
    Object src = e.getSource();
    for (int i=0; i<range.length; i++) {
      if (src == range[i]) {
        updateRange(i);
        return;
      }
    }
  }

  // -- Helper methods - GUI component update --

  /** Updates cache source to match the state of the GUI. */
  private void updateSource() {
    try {
      ICacheSource source = sourceValue(sourceChooser.getSelectedIndex());
      if (source != null) cache.setSource(source);
    }
    catch (CacheException exc) { LOGGER.info("", exc); }
  }

  /** Updates cache strategy to match the state of the GUI. */
  private void updateStrategy() {
    try {
      ICacheStrategy strategy =
        strategyValue(strategyChooser.getSelectedIndex());
      if (strategy != null) cache.setStrategy(strategy);
    }
    catch (CacheException exc) { LOGGER.info("", exc); }
  }

  /** Updates cache range to match the state of the GUI. */
  private void updateRange(int index) {
    int rng = ((Integer) range[index].getValue()).intValue();

    ICacheStrategy strategy = cache.getStrategy();
    int[] ranges = strategy.getRange();
    if (rng != ranges[index]) strategy.setRange(rng, index);
  }

  /** Updates cache priority to match the state of the GUI. */
  private void updatePriority(int index) {
    int prio = priorityValue(priority[index].getSelectedIndex());
    ICacheStrategy strategy = cache.getStrategy();
    int[] priorities = strategy.getPriorities();
    if (prio != priorities[index]) strategy.setPriority(prio, index);
  }

  /** Updates cache order to match the state of the GUI. */
  private void updateOrder(int index) {
    int ord = orderValue(order[index].getSelectedIndex());
    ICacheStrategy strategy = cache.getStrategy();
    int[] orders = strategy.getOrder();
    if (ord != orders[index]) strategy.setOrder(ord, index);
  }

  // -- Helper methods - data conversion --

  /** Converts cache source to source chooser index. */
  private int sourceIndex(ICacheSource s) {
    Class c = s.getClass();
    for (int i=0; i<SOURCE_VALUES.length; i++) {
      if (SOURCE_VALUES[i] == c) return i;
    }
    return -1;
  }

  /** Generates a new cache source matching the source chooser index. */
  private ICacheSource sourceValue(int index) {
    Class c = SOURCE_VALUES[index];
    if (c == cache.getSource().getClass()) return null;
    try {
      Constructor con = c.getConstructor(SOURCE_PARAMS);
      return (ICacheSource) con.newInstance(new Object[] {id});
    }
    catch (NoSuchMethodException exc) { LOGGER.info("", exc); }
    catch (InstantiationException exc) { LOGGER.info("", exc); }
    catch (IllegalAccessException exc) { LOGGER.info("", exc); }
    catch (IllegalArgumentException exc) { LOGGER.info("", exc); }
    catch (InvocationTargetException exc) { LOGGER.info("", exc); }
    return null;
  }

  /** Converts cache strategy to strategy chooser index. */
  private int strategyIndex(ICacheStrategy s) {
    Class c = s.getClass();
    for (int i=0; i<STRATEGY_VALUES.length; i++) {
      if (STRATEGY_VALUES[i] == c) return i;
    }
    return -1;
  }

  /** Generates a new cache strategy matching the strategy chooser index. */
  private ICacheStrategy strategyValue(int index) {
    Class c = STRATEGY_VALUES[index];
    if (c == cache.getStrategy().getClass()) return null;
    try {
      Constructor con = c.getConstructor(STRATEGY_PARAMS);
      return (ICacheStrategy) con.newInstance(new Object[] {lengths});
    }
    catch (NoSuchMethodException exc) { LOGGER.info("", exc); }
    catch (InstantiationException exc) { LOGGER.info("", exc); }
    catch (IllegalAccessException exc) { LOGGER.info("", exc); }
    catch (IllegalArgumentException exc) { LOGGER.info("", exc); }
    catch (InvocationTargetException exc) { LOGGER.info("", exc); }
    return null;
  }

  /** Converts enumerated priority value to priority chooser index. */
  private int priorityIndex(int prio) {
    for (int i=0; i<PRIORITY_VALUES.length; i++) {
      if (PRIORITY_VALUES[i] == prio) return i;
    }
    return -1;
  }

  /** Converts priority chooser index to enumerated priority value. */
  private int priorityValue(int index) { return PRIORITY_VALUES[index]; }

  /** Converts enumerated order value to order chooser index. */
  private int orderIndex(int ord) {
    for (int i=0; i<ORDER_VALUES.length; i++) {
      if (ORDER_VALUES[i] == ord) return i;
    }
    return -1;
  }

  /** Converts order chooser index to enumerated order value. */
  private int orderValue(int index) { return ORDER_VALUES[index]; }

}
