/*
 * #%L
 * Bio-Formats Plugins for ImageJ: a collection of ImageJ plugins including the
 * Bio-Formats Importer, Bio-Formats Exporter, Bio-Formats Macro Extensions,
 * Data Browser and Stack Slicer.
 * %%
 * Copyright (C) 2006 - 2017 Open Microscopy Environment:
 *   - Board of Regents of the University of Wisconsin-Madison
 *   - Glencoe Software, Inc.
 *   - University of Dundee
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 2 of the 
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-2.0.html>.
 * #L%
 */

package loci.plugins.in;

import ij.Macro;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import loci.common.Location;
import loci.common.Region;
import loci.plugins.prefs.DoubleOption;
import loci.plugins.prefs.OptionsList;
import loci.plugins.prefs.StringOption;
import loci.plugins.util.LibraryChecker;

/**
 * Helper class for managing Bio-Formats Importer options.
 * Gets default parameter values from IJ_Prefs.txt.
 */
public class ImporterOptions extends OptionsList {

  // -- Constants --

  // option keys
  public static final String KEY_AUTOSCALE       = "autoscale";
  public static final String KEY_COLOR_MODE      = "colorMode";
  public static final String KEY_CONCATENATE     = "concatenate";
  public static final String KEY_CROP            = "crop";
  public static final String KEY_FIRST           = "firstTime";
  public static final String KEY_FORCE_THUMBS    = "forceThumbnails";
  public static final String KEY_GROUP_FILES     = "groupFiles";
  public static final String KEY_UNGROUP_FILES   = "ungroupFiles";
  public static final String KEY_ID              = "id";
  public static final String KEY_LOCATION        = "location";
  public static final String KEY_OPEN_ALL_SERIES = "openAllSeries";
  public static final String KEY_QUIET           = "quiet";
  //public static final String KEY_RECORD          = "record";
  public static final String KEY_SHOW_METADATA   = "showMetadata";
  public static final String KEY_SHOW_OME_XML    = "showOMEXML";
  public static final String KEY_SHOW_ROIS       = "showROIs";
  public static final String KEY_ROIS_MODE       = "roiMode";
  public static final String KEY_SPECIFY_RANGES  = "specifyRanges";
  public static final String KEY_SPLIT_Z         = "splitFocalPlanes";
  public static final String KEY_SPLIT_T         = "splitTimepoints";
  public static final String KEY_SPLIT_C         = "splitWindows";
  public static final String KEY_STACK_FORMAT    = "stackFormat";
  public static final String KEY_STACK_ORDER     = "stackOrder";
  public static final String KEY_SWAP_DIMS       = "swapDimensions";
  public static final String KEY_UPGRADE_CHECK   = "upgradeCheck";
  public static final String KEY_VIRTUAL         = "virtual";
  public static final String KEY_WINDOWLESS      = "windowless";
  public static final String KEY_STITCH_TILES    = "stitchTiles";
  public static final String KEY_MUST_GROUP    	 = "mustGroup";
  
  // possible values for colorMode
  public static final String COLOR_MODE_DEFAULT = "Default";
  public static final String COLOR_MODE_COMPOSITE = "Composite";
  public static final String COLOR_MODE_COLORIZED = "Colorized";
  public static final String COLOR_MODE_GRAYSCALE = "Grayscale";
  public static final String COLOR_MODE_CUSTOM = "Custom";

  // possible values for location
  public static final String LOCATION_LOCAL = "Local machine";
  public static final String LOCATION_HTTP  = "Internet";
  public static final String LOCATION_OMERO = "OMERO";

  //possible values for roiMode
  public static final String ROIS_MODE_MANAGER = "ROI manager";
  public static final String ROIS_MODE_OVERLAY = "Overlay";
  
  // possible values for stackFormat
  public static final String VIEW_NONE       = "Metadata only";
  public static final String VIEW_STANDARD   = "Standard ImageJ";
  public static final String VIEW_HYPERSTACK = "Hyperstack";
  public static final String VIEW_BROWSER    = "Data Browser";
  public static final String VIEW_IMAGE_5D   = "Image5D";
  public static final String VIEW_VIEW_5D    = "View5D";

  // class to check for each stackFormat value
  private static final String CLASS_IMAGE_5D = "i5d.Image5D";
  private static final String CLASS_VIEW_5D  = "view5d.View5D_";

  // possible values for stackOrder
  public static final String ORDER_DEFAULT = "Default";
  public static final String ORDER_XYZCT   = "XYZCT";
  public static final String ORDER_XYZTC   = "XYZTC";
  public static final String ORDER_XYCZT   = "XYCZT";
  public static final String ORDER_XYTCZ   = "XYTCZ";
  public static final String ORDER_XYCTZ   = "XYCTZ";
  public static final String ORDER_XYTZC   = "XYTZC";

  /** Default custom colors for each channel. */
  private static final Color[] DEFAULT_COLORS = {
    Color.red,
    Color.green,
    Color.blue,
    Color.white,
    Color.cyan,
    Color.magenta,
    Color.yellow
  };

  // -- Fields -- secondary values --

  // series options
  private List<Boolean> seriesOn = new ArrayList<Boolean>();

  // swap options
  private List<String> inputOrder = new ArrayList<String>();

  // range options
  private List<Integer> cBegin = new ArrayList<Integer>();
  private List<Integer> cEnd = new ArrayList<Integer>();
  private List<Integer> cStep = new ArrayList<Integer>();
  private List<Integer> zBegin = new ArrayList<Integer>();
  private List<Integer> zEnd = new ArrayList<Integer>();
  private List<Integer> zStep = new ArrayList<Integer>();
  private List<Integer> tBegin = new ArrayList<Integer>();
  private List<Integer> tEnd = new ArrayList<Integer>();
  private List<Integer> tStep = new ArrayList<Integer>();

  // crop options
  private List<Region> cropRegion = new ArrayList<Region>();

  // color mode options
  private List<List<DoubleOption>> customColors =
    new ArrayList<List<DoubleOption>>();

  // whether to treat the given id as a file pattern directly
  private boolean usePatternIds;

  // -- Constructor --

  public ImporterOptions() throws IOException {
    super("importer-options.txt", ImporterOptions.class);

    // remove unavailable stack formats
    StringOption stackFormat = getStringOption(KEY_STACK_FORMAT);
    if (!LibraryChecker.checkClass(CLASS_IMAGE_5D)) {
      stackFormat.removePossible(VIEW_IMAGE_5D);
    }
    if (!LibraryChecker.checkClass(CLASS_VIEW_5D)) {
      stackFormat.removePossible(VIEW_VIEW_5D);
    }
  }

  // -- ImporterOptions methods - option harvesting --

  /** Parses the plugin argument for parameter values. */
  public void parseArg(String arg) {
    if (arg == null || arg.length() == 0) return;
    if (new Location(arg).exists()) {
      // old style arg: entire argument is a file path

      // this style is used by the HandleExtraFileTypes plugin

      // NB: This functionality must not be removed, or the plugin
      // will stop working correctly with HandleExtraFileTypes.

      setLocation(LOCATION_LOCAL);
      setId(arg);
      setQuiet(true); // suppress obnoxious error messages and such
    }
    else {
      // new style arg: split up similar to a macro options string, but
      // slightly different than macro options, in that boolean arguments
      // must be of the form "key=true" rather than just "key"

      // only the core options are supported for now

      // NB: This functionality enables multiple plugin entries to achieve
      // distinct behavior by calling the LociImporter plugin differently.

      parseOptions(arg);
    }
  }

  /** Handles obsolete macro keys, for backward compatibility. */
  public void checkObsoleteOptions() {
    final String macroOptions = Macro.getOptions();

    // NB: It would be nice to remove the Standard ImageJ option someday;
    // when that happens, the following code provides support for old macros.
    // check obsolete view options
    //String stackFormat = macroOptions == null ?
    //  null : Macro.getValue(macroOptions, "view", null);
    //final String viewStandard = "Standard ImageJ";
    //if (viewStandard.equals(stackFormat)) {
    //  // Standard ImageJ -> Hyperstack
    //  macroOptions = macroOptions.replaceFirst(
    //    "\\[" + viewStandard + "\\]", VIEW_HYPERSTACK);
    //  Macro.setOptions(macroOptions);
    //  setStackFormat(VIEW_HYPERSTACK);
    //}

    // check obsolete color options
    final boolean mergeChannels = checkKey(macroOptions, "merge_channels");
    final boolean rgbColorize = checkKey(macroOptions, "rgb_colorize");
    final boolean customColorize = checkKey(macroOptions, "custom_colorize");
    if (mergeChannels) setColorMode(COLOR_MODE_COMPOSITE);
    else if (rgbColorize) setColorMode(COLOR_MODE_COLORIZED);
    else if (customColorize) setColorMode(COLOR_MODE_CUSTOM);
  }

  // -- ImporterOptions methods - base options accessors and mutators --

  // autoscale
  public String getAutoscaleInfo() { return getInfo(KEY_AUTOSCALE); }
  public boolean isAutoscale() { return isSet(KEY_AUTOSCALE); }
  public void setAutoscale(boolean b) { setValue(KEY_AUTOSCALE, b); }

  // colorMode
  public String getColorModeInfo() { return getInfo(KEY_COLOR_MODE); }
  public String getColorMode() { return getValue(KEY_COLOR_MODE); }
  public String[] getColorModes() { return getPossible(KEY_COLOR_MODE); }
  public boolean isColorModeDefault() {
    return COLOR_MODE_DEFAULT.equals(getColorMode());
  }
  public boolean isColorModeComposite() {
    return COLOR_MODE_COMPOSITE.equals(getColorMode());
  }
  public boolean isColorModeColorized() {
    return COLOR_MODE_COLORIZED.equals(getColorMode());
  }
  public boolean isColorModeGrayscale() {
    return COLOR_MODE_GRAYSCALE.equals(getColorMode());
  }
  public boolean isColorModeCustom() {
    return COLOR_MODE_CUSTOM.equals(getColorMode());
  }
  public void setColorMode(String s) { setValue(KEY_COLOR_MODE, s); }

  // concatenate
  public String getConcatenateInfo() { return getInfo(KEY_CONCATENATE); }
  public boolean isConcatenate() { return isSet(KEY_CONCATENATE); }
  public void setConcatenate(boolean b) { setValue(KEY_CONCATENATE, b); }

  // crop
  public String getCropInfo() { return getInfo(KEY_CROP); }
  public boolean doCrop() { return isSet(KEY_CROP); }
  public void setCrop(boolean b) { setValue(KEY_CROP, b); }

  // firstTime
  public String getFirstTimeInfo() { return getInfo(KEY_FIRST); }
  public boolean isFirstTime() { return isSet(KEY_FIRST); }
  public void setFirstTime(boolean b) { setValue(KEY_FIRST, b); }

  // forceThumbnails
  public String getForceThumbnailsInfo() { return getInfo(KEY_FORCE_THUMBS); }
  public boolean isForceThumbnails() { return isSet(KEY_FORCE_THUMBS); }
  public void setForceThumbnails(boolean b) { setValue(KEY_FORCE_THUMBS, b); }

  // groupFiles
  public String getGroupFilesInfo() { return getInfo(KEY_GROUP_FILES); }
  public boolean isGroupFiles() { return isSet(KEY_GROUP_FILES); }
  public void setGroupFiles(boolean b) { setValue(KEY_GROUP_FILES, b); }

  // ungroupFiles
  public String getUngroupFilesInfo() { return getInfo(KEY_UNGROUP_FILES); }
  public boolean isUngroupFiles() { return isSet(KEY_UNGROUP_FILES); }
  public void setUngroupFiles(boolean b) { setValue(KEY_UNGROUP_FILES, b); }

  // id
  public String getIdInfo() { return getInfo(KEY_ID); }
  public String getId() { return getValue(KEY_ID); }
  public void setId(String s) { setValue(KEY_ID, s); }

  // location
  public String getLocationInfo() { return getInfo(KEY_LOCATION); }
  public String getLocation() { return getValue(KEY_LOCATION); }
  public String[] getLocations() { return getPossible(KEY_LOCATION); }
  public boolean isLocal() { return LOCATION_LOCAL.equals(getLocation()); }
  public boolean isHTTP() { return LOCATION_HTTP.equals(getLocation()); }
  public boolean isOMERO() { return LOCATION_OMERO.equals(getLocation()); }
  public void setLocation(String s) { setValue(KEY_LOCATION, s); }

  // openAllSeries
  public String getOpenAllSeriesInfo() { return getInfo(KEY_OPEN_ALL_SERIES); }
  public boolean openAllSeries() { return isSet(KEY_OPEN_ALL_SERIES); }
  public void setOpenAllSeries(boolean b) { setValue(KEY_OPEN_ALL_SERIES, b); }

  // quiet
  public String getQuietInfo() { return getInfo(KEY_QUIET); }
  public boolean isQuiet() { return isSet(KEY_QUIET); }
  public void setQuiet(boolean b) { setValue(KEY_QUIET, b); }

  // record
  //public String getRecordInfo() { return getInfo(KEY_RECORD); }
  //public boolean isRecord() { return isSet(KEY_RECORD); }
  //public void setRecord(boolean b) { setValue(KEY_RECORD, b); }

  // showMetadata
  public String getShowMetadataInfo() { return getInfo(KEY_SHOW_METADATA); }
  public boolean isShowMetadata() { return isSet(KEY_SHOW_METADATA); }
  public void setShowMetadata(boolean b) { setValue(KEY_SHOW_METADATA, b); }

  // showOMEXML
  public String getShowOMEXMLInfo() { return getInfo(KEY_SHOW_OME_XML); }
  public boolean isShowOMEXML() { return isSet(KEY_SHOW_OME_XML); }
  public void setShowOMEXML(boolean b) { setValue(KEY_SHOW_OME_XML, b); }

  // showROIs
  public String getShowROIsInfo() { return getInfo(KEY_SHOW_ROIS); }
  public boolean showROIs() { return isSet(KEY_SHOW_ROIS); }
  public void setShowROIs(boolean b) { setValue(KEY_SHOW_ROIS, b); }

  // roisMode
 public String getROIsModeInfo() { return getInfo(KEY_ROIS_MODE); }
 public String getROIsMode() { return getValue(KEY_ROIS_MODE); }
 public String[] getROIsModes() { return getPossible(KEY_ROIS_MODE); }
 public void setROIsMode(String s) { setValue(KEY_ROIS_MODE, s); }
  
  // specifyRanges
  public String getSpecifyRangesInfo() { return getInfo(KEY_SPECIFY_RANGES); }
  public boolean isSpecifyRanges() { return isSet(KEY_SPECIFY_RANGES); }
  public void setSpecifyRanges(boolean b) { setValue(KEY_SPECIFY_RANGES, b); }

  // splitFocalPlanes
  public String getSplitFocalPlanesInfo() { return getInfo(KEY_SPLIT_Z); }
  public boolean isSplitFocalPlanes() { return isSet(KEY_SPLIT_Z); }
  public void setSplitFocalPlanes(boolean b) { setValue(KEY_SPLIT_Z, b); }

  // splitTimepoints
  public String getSplitTimepointsInfo() { return getInfo(KEY_SPLIT_T); }
  public boolean isSplitTimepoints() { return isSet(KEY_SPLIT_T); }
  public void setSplitTimepoints(boolean b) { setValue(KEY_SPLIT_T, b); }

  // splitWindows
  public String getSplitChannelsInfo() { return getInfo(KEY_SPLIT_C); }
  public boolean isSplitChannels() { return isSet(KEY_SPLIT_C); }
  public void setSplitChannels(boolean b) { setValue(KEY_SPLIT_C, b); }

  // stackFormat
  public String getStackFormatInfo() { return getInfo(KEY_STACK_FORMAT); }
  public String getStackFormat() { return getValue(KEY_STACK_FORMAT); }
  public String[] getStackFormats() { return getPossible(KEY_STACK_FORMAT); }
  public boolean isViewNone() { return VIEW_NONE.equals(getStackFormat()); }
  public boolean isViewStandard() {
    return VIEW_STANDARD.equals(getStackFormat());
  }
  public boolean isViewHyperstack() {
    return VIEW_HYPERSTACK.equals(getStackFormat());
  }
  public boolean isViewBrowser() {
    return VIEW_BROWSER.equals(getStackFormat());
  }
  public boolean isViewImage5D() {
    return VIEW_IMAGE_5D.equals(getStackFormat());
  }
  public boolean isViewView5D() {
    return VIEW_VIEW_5D.equals(getStackFormat());
  }
  public void setStackFormat(String s) { setValue(KEY_STACK_FORMAT, s); }

  // stackOrder
  public String getStackOrderInfo() { return getInfo(KEY_STACK_ORDER); }
  public String getStackOrder() { return getValue(KEY_STACK_ORDER); }
  public String[] getStackOrders() { return getPossible(KEY_STACK_ORDER); }
  public void setStackOrder(String s) { setValue(KEY_STACK_ORDER, s); }

  // swapDimensions
  public String getSwapDimensionsInfo() { return getInfo(KEY_SWAP_DIMS); }
  public boolean isSwapDimensions() { return isSet(KEY_SWAP_DIMS); }
  public void setSwapDimensions(boolean b) { setValue(KEY_SWAP_DIMS, b); }

  // upgradeCheck
  public String getUpgradeCheckInfo() { return getInfo(KEY_UPGRADE_CHECK); }
  public boolean doUpgradeCheck() { return isSet(KEY_UPGRADE_CHECK); }
  public void setUpgradeCheck(boolean b) { setValue(KEY_UPGRADE_CHECK, b); }

  // virtual
  public String getVirtualInfo() { return getInfo(KEY_VIRTUAL); }
  public boolean isVirtual() { return isSet(KEY_VIRTUAL); }
  public void setVirtual(boolean b) { setValue(KEY_VIRTUAL, b); }

  // windowless
  public String getWindowlessInfo() { return getInfo(KEY_WINDOWLESS); }
  public boolean isWindowless() { return isSet(KEY_WINDOWLESS); }
  public void setWindowless(boolean b) { setValue(KEY_WINDOWLESS, b); }

  // stitchTiles
  public String getStitchTilesInfo() { return getInfo(KEY_STITCH_TILES); }
  public boolean doStitchTiles() { return isSet(KEY_STITCH_TILES); }
  public void setStitchTiles(boolean b) { setValue(KEY_STITCH_TILES, b); }

  // mustGroup
  public String getMustGroupInfo() { return getInfo(KEY_MUST_GROUP); }
  public boolean doMustGroup() { return isSet(KEY_MUST_GROUP); }
  public void setMustGroup(boolean b) { setValue(KEY_MUST_GROUP, b); }
  
  // -- ImporterOptions methods - secondary options accessors and mutators --

  // series options
  public boolean isSeriesOn(int s) {
    if (openAllSeries() || isViewNone()) return true;
    return get(seriesOn, s, s == 0);
  }
  public void setSeriesOn(int s, boolean value) {
    set(seriesOn, s, value, false);
  }
  public void clearSeries() {
    seriesOn.clear();
  }

  // swap options
  public String getInputOrder(int s) { return get(inputOrder, s, null); }
  public void setInputOrder(int s, String value) {
    set(inputOrder, s, value, null);
  }

  // range options
  public int getCBegin(int s) { return get(cBegin, s, 0); }
  public void setCBegin(int s, int value) { set(cBegin, s, value, 0); }
  public int getCEnd(int s) { return get(cEnd, s, -1); }
  public void setCEnd(int s, int value) { set(cEnd, s, value, -1); }
  public int getCStep(int s) { return get(cStep, s, 1); }
  public void setCStep(int s, int value) {
    if (value <= 0) {
      throw new IllegalArgumentException("Invalid C step: " + value);
    }
    set(cStep, s, value, 1);
  }

  public int getZBegin(int s) { return get(zBegin, s, 0); }
  public void setZBegin(int s, int value) { set(zBegin, s, value, 0); }
  public int getZEnd(int s) { return get(zEnd, s, -1); }
  public void setZEnd(int s, int value) { set(zEnd, s, value, -1); }
  public int getZStep(int s) { return get(zStep, s, 1); }
  public void setZStep(int s, int value) {
    if (value <= 0) {
      throw new IllegalArgumentException("Invalid Z step: " + value);
    }
    set(zStep, s, value, 1);
  }

  public int getTBegin(int s) { return get(tBegin, s, 0); }
  public void setTBegin(int s, int value) { set(tBegin, s, value, 0); }
  public int getTEnd(int s) { return get(tEnd, s, -1); }
  public void setTEnd(int s, int value) { set(tEnd, s, value, -1); }
  public int getTStep(int s) { return get(tStep, s, 1); }
  public void setTStep(int s, int value) {
    if (value <= 0) {
      throw new IllegalArgumentException("Invalid T step: " + value);
    }
    set(tStep, s, value, 1);
  }

  // crop options
  public Region getCropRegion(int s) { return get(cropRegion, s, null); }
  public void setCropRegion(int s, Region r) { set(cropRegion, s, r, null); }

  // color mode options
  public Color getCustomColor(int s, int c) {
    List<DoubleOption> list = get(customColors, s, null);
    int defaultColor = getDefaultCustomColor(c).getRGB();
    DoubleOption color = null;
    if (list != null) {
      color = get(list, c, null);
    }
    if (color == null) {
      color =
        new DoubleOption(getCustomColorKey(s, c), true, "", "", defaultColor);
      color.loadOption();
    }
    return new Color((int) color.getValue());
  }
  public void setCustomColor(int s, int c, Color color) {
    List<DoubleOption> list = get(customColors, s, null);
    if (list == null) {
      list = new ArrayList<DoubleOption>();
      set(customColors, s, list, null);
    }
    DoubleOption colorOption = get(list, c, null);
    if (colorOption == null) {
      colorOption = new DoubleOption(getCustomColorKey(s, c), true, "", "", 0);
    }
    colorOption.setValue(color.getRGB());
    set(list, c, colorOption, null);
    colorOption.saveOption();
  }
  public Color getDefaultCustomColor(int c) {
    return DEFAULT_COLORS[c % DEFAULT_COLORS.length];
  }
  public String getCustomColorKey(int s, int c) {
    return s + "_" + c;
  }

  public boolean isUsingPatternIds() { return usePatternIds; }
  public void setUsingPatternIds(boolean b) { usePatternIds = b; }

  // -- Helper methods --

  private <T extends Object> void set(List<T> list,
    int index, T value, T fillValue)
  {
    while (list.size() <= index) list.add(fillValue);
    list.set(index, value);
  }

  private <T extends Object> T get(List<T> list, int index, T defaultValue) {
    if (list.size() <= index) return defaultValue;
    return list.get(index);
  }

  /** Tests whether the given boolean key is set in the specified options. */
  protected boolean checkKey(String options, String key) {
    if (options == null) return false;

    // delete anything inside square brackets, for simplicity
    while (true) {
      int lIndex = options.indexOf('[');
      if (lIndex < 0) break;
      int rIndex = options.indexOf(']');
      if (rIndex < 0) rIndex = options.length() - 1;
      options = options.substring(0, lIndex) + options.substring(rIndex + 1);
    }

    // split the options string
    final String[] tokens = options.split(" ");

    // search for a token matching the key
    for (String token : tokens) {
      if (token.equals(key)) return true;
    }
    return false;
  }

}
