/*
 PreferencesMap - A Map<String, String> with some useful features 
 to handle preferences.
 Part of the Arduino project - http://www.arduino.cc/

 Copyright (c) 2014 Cristian Maglie

 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, write to the Free Software Foundation,
 Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package processing.app.helpers;

import org.apache.commons.compress.utils.IOUtils;
import processing.app.legacy.PApplet;

import java.io.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

@SuppressWarnings("serial")
public class PreferencesMap extends LinkedHashMap<String, String> {

  public PreferencesMap(Map<String, String> table) {
    super(table);
  }

  /**
   * Create a PreferencesMap and load the content of the file passed as
   * argument.
   * 
   * Is equivalent to:
   * 
   * <pre>
   * PreferencesMap map = new PreferencesMap();
   * map.load(file);
   * </pre>
   * 
   * @param file
   * @throws IOException
   */
  public PreferencesMap(File file) throws IOException {
    super();
    load(file);
  }

  public PreferencesMap() {
    super();
  }

  /**
   * Parse a property list file and put kev/value pairs into the Map
   * 
   * @param file
   * @throws FileNotFoundException
   * @throws IOException
   */
  public void load(File file) throws IOException {
    FileInputStream fileInputStream = null;
    try {
      fileInputStream = new FileInputStream(file);
      load(fileInputStream);
    } finally {
      IOUtils.closeQuietly(fileInputStream);
    }
  }

  protected String processPlatformSuffix(String key, String suffix, boolean isCurrentPlatform) {
    if (key == null)
      return null;
    // Key does not end with the given suffix? Process as normal
    if (!key.endsWith(suffix))
      return key;
    // Not the current platform? Ignore this key
    if (!isCurrentPlatform)
      return null;
    // Strip the suffix from the key
    return key.substring(0, key.length() - suffix.length());
  }

  /**
   * Parse a property list stream and put key/value pairs into the Map
   * 
   * @param input
   * @throws IOException
   */
  public void load(InputStream input) throws IOException {
    String[] lines = PApplet.loadStrings(input);
    for (String line : lines) {
      if (line.length() == 0 || line.charAt(0) == '#')
        continue;

      int equals = line.indexOf('=');
      if (equals != -1) {
        String key = line.substring(0, equals).trim();
        String value = line.substring(equals + 1).trim();

        key = processPlatformSuffix(key, ".linux", OSUtils.isLinux());
        key = processPlatformSuffix(key, ".windows", OSUtils.isWindows());
        key = processPlatformSuffix(key, ".macosx", OSUtils.isMacOS());

        if (key != null)
          put(key, value);
      }
    }
  }

  /**
   * Create a new PreferenceMap that contains all the top level pairs of the
   * current mapping. E.g. the folowing mapping:<br />
   * 
   * <pre>
   * Map (
   *     alpha = Alpha
   *     alpha.some.keys = v1
   *     alpha.other.keys = v2
   *     beta = Beta
   *     beta.some.keys = v3
   *   )
   * </pre>
   * 
   * will generate the following result:
   * 
   * <pre>
   * Map (
   *     alpha = Alpha
   *     beta = Beta
   *   )
   * </pre>
   * 
   * @return
   */
  public PreferencesMap topLevelMap() {
    PreferencesMap res = new PreferencesMap();
    for (String key : keySet()) {
      if (key.contains("."))
        continue;
      res.put(key, get(key));
    }
    return res;
  }

  /**
   * Create a new Map<String, PreferenceMap> where keys are the first level of
   * the current mapping. Top level pairs are discarded. E.g. the folowing
   * mapping:<br />
   * 
   * <pre>
   * Map (
   *     alpha = Alpha
   *     alpha.some.keys = v1
   *     alpha.other.keys = v2
   *     beta = Beta
   *     beta.some.keys = v3
   *   )
   * </pre>
   * 
   * will generate the following result:
   * 
   * <pre>
   * alpha = Map(
   *     some.keys = v1
   *     other.keys = v2
   *   )
   * beta = Map(
   *     some.keys = v3
   *   )
   * </pre>
   * 
   * @return
   */
  public Map<String, PreferencesMap> firstLevelMap() {
    Map<String, PreferencesMap> res = new LinkedHashMap<>();
    for (String key : keySet()) {
      int dot = key.indexOf('.');
      if (dot == -1)
        continue;

      String parent = key.substring(0, dot);
      String child = key.substring(dot + 1);

      if (!res.containsKey(parent))
        res.put(parent, new PreferencesMap());
      res.get(parent).put(child, get(key));
    }
    return res;
  }

  /**
   * Create a new PreferenceMap using a subtree of the current mapping. Top
   * level pairs are ignored. E.g. with the following mapping:<br />
   * 
   * <pre>
   * Map (
   *     alpha = Alpha
   *     alpha.some.keys = v1
   *     alpha.other.keys = v2
   *     beta = Beta
   *     beta.some.keys = v3
   *   )
   * </pre>
   * 
   * a call to createSubTree("alpha") will generate the following result:
   * 
   * <pre>
   * Map(
   *     some.keys = v1
   *     other.keys = v2
   *   )
   * </pre>
   * 
   * @param parent
   * @return
   */
  public PreferencesMap subTree(String parent) {
    return subTree(parent, -1);
  }

  public PreferencesMap subTree(String parent, int sublevels) {
    PreferencesMap res = new PreferencesMap();
    parent += ".";
    int parentLen = parent.length();
    for (String key : keySet()) {
      if (key.startsWith(parent)) {
        String newKey = key.substring(parentLen);
        int keySubLevels = newKey.split("\\.").length;
        if (sublevels == -1 || keySubLevels == sublevels) {
          res.put(newKey, get(key));
        }
      }
    }
    return res;
  }

  public String toString(String indent) {
    String res = indent + "{\n";
    SortedSet<String> treeSet = new TreeSet<>(keySet());
    for (String k : treeSet)
      res += indent + "  " + k + " = " + get(k) + "\n";
    res += indent + "}\n";
    return res;
  }

  /**
   * Returns the value to which the specified key is mapped, or throws a
   * PreferencesMapException if not found
   * 
   * @param k
   *          the key whose associated value is to be returned
   * @return the value to which the specified key is mapped
   * @throws PreferencesMapException
   */
  public String getOrExcept(String k) throws PreferencesMapException {
    String r = get(k);
    if (r == null)
      throw new PreferencesMapException(k);
    return r;
  }

  @Override
  public String toString() {
    return toString("");
  }

  /**
   * Creates a new File instance by converting the value of the key into an
   * abstract pathname. If the the given key doesn't exists or his value is the
   * empty string, the result is <b>null</b>.
   * 
   * @param key
   * @return
   */
  public File getFile(String key) {
    if (!containsKey(key))
      return null;
    String path = get(key).trim();
    if (path.length() == 0)
      return null;
    return new File(path);
  }

  /**
   * Creates a new File instance by converting the value of the key into an
   * abstract pathname with the specified sub folder. If the the given key
   * doesn't exists or his value is the empty string, the result is <b>null</b>.
   * 
   * @param key
   * @param subFolder
   * @return
   */
  public File getFile(String key, String subFolder) {
    File file = getFile(key);
    if (file == null)
      return null;
    return new File(file, subFolder);
  }

  /**
   * Return the value of the specified key as boolean.
   * 
   * @param key
   * @return <b>true</b> if the value of the key is the string "true" (case
   *         insensitive compared), <b>false</b> in any other case
   */
  public boolean getBoolean(String key) {
    return Boolean.valueOf(get(key));
  }

  /**
   * Sets the value of the specified key to the string <b>"true"</b> or
   * <b>"false"</b> based on value of the boolean parameter
   * 
   * @param key
   * @param value
   * @return <b>true</b> if the previous value of the key was the string "true"
   *         (case insensitive compared), <b>false</b> in any other case
   */
  public boolean putBoolean(String key, boolean value) {
    String prev = put(key, value ? "true" : "false");
    return new Boolean(prev);
  }

  public String get(String key, String defaultValue) {
    String value = get(key);
    if (value != null) {
      return value;
    }
    return defaultValue;
  }

}
