/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.zeppelin.display;

import org.apache.commons.lang.StringUtils;

import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Input type.
 */
public class Input implements Serializable {
  /**
   * Parameters option.
   */
  public static class ParamOption {
    Object value;
    String displayName;

    public ParamOption(Object value, String displayName) {
      super();
      this.value = value;
      this.displayName = displayName;
    }

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

      ParamOption that = (ParamOption) o;

      if (value != null ? !value.equals(that.value) : that.value != null) return false;
      return displayName != null ? displayName.equals(that.displayName) : that.displayName == null;

    }

    @Override
    public int hashCode() {
      int result = value != null ? value.hashCode() : 0;
      result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
      return result;
    }

    public Object getValue() {
      return value;
    }

    public void setValue(Object value) {
      this.value = value;
    }

    public String getDisplayName() {
      return displayName;
    }

    public void setDisplayName(String displayName) {
      this.displayName = displayName;
    }

  }

  String name;
  String displayName;
  String type;
  String argument;
  Object defaultValue;
  ParamOption[] options;
  boolean hidden;

  public Input(String name, Object defaultValue, String type) {
    this.name = name;
    this.displayName = name;
    this.defaultValue = defaultValue;
    this.type = type;
  }

  public Input(String name, Object defaultValue, String type, ParamOption[] options) {
    this.name = name;
    this.displayName = name;
    this.defaultValue = defaultValue;
    this.type = type;
    this.options = options;
  }

  public Input(String name, String displayName, String type, String argument, Object defaultValue,
      ParamOption[] options, boolean hidden) {
    super();
    this.name = name;
    this.displayName = displayName;
    this.argument = argument;
    this.type = type;
    this.defaultValue = defaultValue;
    this.options = options;
    this.hidden = hidden;
  }

  @Override
  public boolean equals(Object o) {
    return name.equals(((Input) o).getName());
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getDisplayName() {
    return displayName;
  }

  public void setDisplayName(String displayName) {
    this.displayName = displayName;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

  public Object getDefaultValue() {
    return defaultValue;
  }

  public void setDefaultValue(Object defaultValue) {
    this.defaultValue = defaultValue;
  }

  public ParamOption[] getOptions() {
    return options;
  }

  public void setOptions(ParamOption[] options) {
    this.options = options;
  }

  public boolean isHidden() {
    return hidden;
  }

  // Syntax of variables: ${TYPE:NAME=DEFAULT_VALUE1|DEFAULT_VALUE2|...,VALUE1|VALUE2|...}
  // Type is optional. Type may contain an optional argument with syntax: TYPE(ARG)
  // NAME and VALUEs may contain an optional display name with syntax: NAME(DISPLAY_NAME)
  // DEFAULT_VALUEs may not contain display name
  // Examples:  ${age}                              input form without default value
  //            ${age=3}                            input form with default value
  //            ${age(Age)=3}                       input form with display name and default value
  //            ${country=US(United States)|UK|JP}  select form with
  //            ${checkbox( or ):country(Country)=US|JP,US(United States)|UK|JP}
  //                                                checkbox form with " or " as delimiter: will be
  //                                                expanded to "US or JP"
  private static final Pattern VAR_PTN = Pattern.compile("([_])?[$][{]([^=}]*([=][^}]*)?)[}]");

  private static String[] getNameAndDisplayName(String str) {
    Pattern p = Pattern.compile("([^(]*)\\s*[(]([^)]*)[)]");
    Matcher m = p.matcher(str.trim());
    if (m == null || m.find() == false) {
      return null;
    }
    String[] ret = new String[2];
    ret[0] = m.group(1);
    ret[1] = m.group(2);
    return ret;
  }

  private static String[] getType(String str) {
    Pattern p = Pattern.compile("([^:()]*)\\s*([(][^()]*[)])?\\s*:(.*)");
    Matcher m = p.matcher(str.trim());
    if (m == null || m.find() == false) {
      return null;
    }
    String[] ret = new String[3];
    ret[0] = m.group(1).trim();
    if (m.group(2) != null) {
      ret[1] = m.group(2).trim().replaceAll("[()]", "");
    }
    ret[2] = m.group(3).trim();
    return ret;
  }

  private static Input getInputForm(Matcher match) {
    String hiddenPart = match.group(1);
    boolean hidden = false;
    if ("_".equals(hiddenPart)) {
      hidden = true;
    }
    String m = match.group(2);

    String namePart;
    String valuePart;

    int p = m.indexOf('=');
    if (p > 0) {
      namePart = m.substring(0, p);
      valuePart = m.substring(p + 1);
    } else {
      namePart = m;
      valuePart = null;
    }


    String varName;
    String displayName = null;
    String type = null;
    String arg = null;
    Object defaultValue = "";
    ParamOption[] paramOptions = null;

    // get var name type
    String varNamePart;
    String[] typeArray = getType(namePart);
    if (typeArray != null) {
      type = typeArray[0];
      arg = typeArray[1];
      varNamePart = typeArray[2];
    } else {
      varNamePart = namePart;
    }

    // get var name and displayname
    String[] varNameArray = getNameAndDisplayName(varNamePart);
    if (varNameArray != null) {
      varName = varNameArray[0];
      displayName = varNameArray[1];
    } else {
      varName = varNamePart.trim();
    }

    // get defaultValue
    if (valuePart != null) {
      // find default value
      int optionP = valuePart.indexOf(",");
      if (optionP >= 0) { // option available
        defaultValue = valuePart.substring(0, optionP);
        if (type != null && type.equals("checkbox")) {
          // checkbox may contain multiple default checks
          defaultValue = Input.splitPipe((String) defaultValue);
        }
        String optionPart = valuePart.substring(optionP + 1);
        String[] options = Input.splitPipe(optionPart);

        paramOptions = new ParamOption[options.length];

        for (int i = 0; i < options.length; i++) {

          String[] optNameArray = getNameAndDisplayName(options[i]);
          if (optNameArray != null) {
            paramOptions[i] = new ParamOption(optNameArray[0], optNameArray[1]);
          } else {
            paramOptions[i] = new ParamOption(options[i], null);
          }
        }


      } else { // no option
        defaultValue = valuePart;
      }

    }

    return new Input(varName, displayName, type, arg, defaultValue, paramOptions, hidden);
  }

  public static LinkedHashMap<String, Input> extractSimpleQueryForm(String script) {
    LinkedHashMap<String, Input> forms = new LinkedHashMap<>();
    if (script == null) {
      return forms;
    }
    String replaced = script;

    Matcher match = VAR_PTN.matcher(replaced);
    while (match.find()) {
      Input form = getInputForm(match);
      forms.put(form.name, form);
    }

    forms.remove("pql");
    return forms;
  }

  private static final String DEFAULT_DELIMITER = ",";

  public static String getSimpleQuery(Map<String, Object> params, String script) {
    String replaced = script;

    Matcher match = VAR_PTN.matcher(replaced);
    while (match.find()) {
      Input input = getInputForm(match);
      Object value;
      if (params.containsKey(input.name)) {
        value = params.get(input.name);
      } else {
        value = input.defaultValue;
      }

      String expanded;
      if (value instanceof Object[] || value instanceof Collection) {  // multi-selection
        String delimiter = input.argument;
        if (delimiter == null) {
          delimiter = DEFAULT_DELIMITER;
        }
        Collection<Object> checked = value instanceof Collection ? (Collection<Object>) value
                : Arrays.asList((Object[]) value);
        List<Object> validChecked = new LinkedList<>();
        for (Object o : checked) {  // filter out obsolete checked values
          for (ParamOption option : input.getOptions()) {
            if (option.getValue().equals(o)) {
              validChecked.add(o);
              break;
            }
          }
        }
        params.put(input.name, validChecked);
        expanded = StringUtils.join(validChecked, delimiter);
      } else {  // single-selection
        expanded = value.toString();
      }
      replaced = match.replaceFirst(expanded);
      match = VAR_PTN.matcher(replaced);
    }

    return replaced;
  }


  public static String[] split(String str) {
    return str.split(";(?=([^\"']*\"[^\"']*\")*[^\"']*$)");

  }

  /*
   * public static String [] splitPipe(String str){ //return
   * str.split("\\|(?=([^\"']*\"[^\"']*\")*[^\"']*$)"); return
   * str.split("\\|(?=([^\"']*\"[^\"']*\")*[^\"']*$)"); }
   */


  public static String[] splitPipe(String str) {
    return split(str, '|');
  }

  public static String[] split(String str, char split) {
    return split(str, new String[] {String.valueOf(split)}, false);
  }

  public static String[] split(String str, String[] splitters, boolean includeSplitter) {
    String escapeSeq = "\"',;${}";
    char escapeChar = '\\';

    String[] blockStart = new String[] {"\"", "'", "${", "N_(", "N_<"};
    String[] blockEnd = new String[] {"\"", "'", "}", "N_)", "N_>"};

    return split(str, escapeSeq, escapeChar, blockStart, blockEnd, splitters, includeSplitter);

  }

  public static String[] split(String str, String escapeSeq, char escapeChar, String[] blockStart,
      String[] blockEnd, String[] splitters, boolean includeSplitter) {

    List<String> splits = new ArrayList<>();

    StringBuilder curString = new StringBuilder();

    boolean escape = false; // true when escape char is found
    int lastEscapeOffset = -1;
    int blockStartPos = -1;
    List<Integer> blockStack = new LinkedList<>();

    for (int i = 0; i < str.length(); i++) {
      char c = str.charAt(i);

      // escape char detected
      if (c == escapeChar && escape == false) {
        escape = true;
        continue;
      }

      // escaped char comes
      if (escape == true) {
        if (escapeSeq.indexOf(c) < 0) {
          curString.append(escapeChar);
        }
        curString.append(c);
        escape = false;
        lastEscapeOffset = curString.length();
        continue;
      }

      if (blockStack.size() > 0) { // inside of block
        curString.append(c);
        // check multichar block
        boolean multicharBlockDetected = false;
        for (int b = 0; b < blockStart.length; b++) {
          if (blockStartPos >= 0
              && getBlockStr(blockStart[b]).compareTo(str.substring(blockStartPos, i)) == 0) {
            blockStack.remove(0);
            blockStack.add(0, b);
            multicharBlockDetected = true;
            break;
          }
        }

        if (multicharBlockDetected == true) {
          continue;
        }

        // check if current block is nestable
        if (isNestedBlock(blockStart[blockStack.get(0)]) == true) {
          // try to find nested block start

          if (curString.substring(lastEscapeOffset + 1).endsWith(
              getBlockStr(blockStart[blockStack.get(0)])) == true) {
            blockStack.add(0, blockStack.get(0)); // block is started
            blockStartPos = i;
            continue;
          }
        }

        // check if block is finishing
        if (curString.substring(lastEscapeOffset + 1).endsWith(
            getBlockStr(blockEnd[blockStack.get(0)]))) {
          // the block closer is one of the splitters (and not nested block)
          if (isNestedBlock(blockEnd[blockStack.get(0)]) == false) {
            for (String splitter : splitters) {
              if (splitter.compareTo(getBlockStr(blockEnd[blockStack.get(0)])) == 0) {
                splits.add(curString.toString());
                if (includeSplitter == true) {
                  splits.add(splitter);
                }
                curString.setLength(0);
                lastEscapeOffset = -1;

                break;
              }
            }
          }
          blockStartPos = -1;
          blockStack.remove(0);
          continue;
        }

      } else { // not in the block
        boolean splitted = false;
        for (String splitter : splitters) {
          // forward check for splitter
          int curentLenght = i + splitter.length();
          if (splitter.compareTo(str.substring(i, Math.min(curentLenght, str.length()))) == 0) {
            splits.add(curString.toString());
            if (includeSplitter == true) {
              splits.add(splitter);
            }
            curString.setLength(0);
            lastEscapeOffset = -1;
            i += splitter.length() - 1;
            splitted = true;
            break;
          }
        }
        if (splitted == true) {
          continue;
        }

        // add char to current string
        curString.append(c);

        // check if block is started
        for (int b = 0; b < blockStart.length; b++) {
          if (curString.substring(lastEscapeOffset + 1)
                       .endsWith(getBlockStr(blockStart[b])) == true) {
            blockStack.add(0, b); // block is started
            blockStartPos = i;
            break;
          }
        }
      }
    }
    if (curString.length() > 0) {
      splits.add(curString.toString().trim());
    }
    return splits.toArray(new String[] {});

  }

  private static String getBlockStr(String blockDef) {
    if (blockDef.startsWith("N_")) {
      return blockDef.substring("N_".length());
    } else {
      return blockDef;
    }
  }

  private static boolean isNestedBlock(String blockDef) {
    if (blockDef.startsWith("N_")) {
      return true;
    } else {
      return false;
    }
  }
}
