/*
 * 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.weex.utils;

import android.support.annotation.NonNull;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Parser for function like "rotate(30 ) transform(50 , 20)".
 * This class will translate the raw string presentation of a group of function(s) to give type
 * according to the {@link FunctionParser.Mapper}
 */
public class FunctionParser<K, V> {

  public static final char SPACE = ' ';

  private Mapper<K, V> mapper;
  private Lexer lexer;

  /**
   * Construct a function parser
   * @param source the raw string representation of a group of function(s)
   * @param mapper the mapping rule between string and corresponding type of object.
   */
  public FunctionParser(@NonNull String source, @NonNull Mapper<K, V> mapper) {
    this.lexer = new Lexer(source);
    this.mapper = mapper;
  }

  /**
   * Start to parse the raw string. The result will be stored in a sorted map where the order
   * is defined by the function order in the raw string.
   * @return
   */
  public LinkedHashMap<K, V> parse() {
    lexer.moveOn();
    return definition();
  }

  private LinkedHashMap<K, V> definition() {
    LinkedHashMap<K, V> result = new LinkedHashMap<>();
    do {
      result.putAll(function());
    } while (lexer.getCurrentToken() == Token.FUNC_NAME);
    return result;
  }

  private Map<K, V> function() {
    List<String> list = new LinkedList<>();
    String functionName = match(Token.FUNC_NAME);
    match(Token.LEFT_PARENT);
    list.add(match(Token.PARAM_VALUE));
    while (lexer.getCurrentToken() == Token.COMMA) {
      match(Token.COMMA);
      list.add(match(Token.PARAM_VALUE));
    }
    match(Token.RIGHT_PARENT);
    return mapper.map(functionName, list);
  }

  private String match(Token token) {
    try {
      if (token == lexer.getCurrentToken()) {
        String value = lexer.getCurrentTokenValue();
        lexer.moveOn();
        return value;
      }
    } catch (Exception e) {
      WXLogUtils.e(token + "Token doesn't match" + lexer.source);
    }
    return "";
  }

  private enum Token {
    FUNC_NAME, PARAM_VALUE, LEFT_PARENT, RIGHT_PARENT, COMMA
  }

  public interface Mapper<K, V> {

    /**
     * Map one function to a specified type of object
     * @param functionName the name of the raw function.
     * @param raw the list of parameter of the raw function
     * @return the expected mapping relationship, where the key in the map is the same as the
     * key in the return value of {{@link #parse()}},
     * and the value in the map is the type of object that expected by user.
     */
    Map<K, V> map(String functionName, List<String> raw);
  }

  private static class WXInterpretationException extends RuntimeException {

    private WXInterpretationException(String msg) {
      super(msg);
    }
  }

  /**
   * Lexer for the parser. For now,  digit, alphabet, '(', ')', '.', ',', '+', '-' and '%' is
   * valid character, and '(', ')', ',', parameter value and function name is valid token.
   * Parameter value is defined as "(?i)[\+-]?[0-9]+(\.[0-9]+)?(%||deg||px)?" while function
   * name is defined as "[a-zA-Z]+".
   *
   * The Lexer can also be expressed using the following EBNF format.
   * <ul>
   *   <li>definition = {function};</li>
   *   <li>function = name, "(", value, { ",", value } , ")";</li>
   *   <li>name = character, {character};</li>
   *   <li>value = identifier, {identifier};</li>
   *   <li>identifier = character | "." | "%" | "+" | "-";</li>
   *   <li>character = digit | letter;</li>
   *   <li>digit =  "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;</li>
   *   <li>letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" |
   *   "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" |
   *   "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" |
   *   "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" ;</li>
   * </ul>
   */
  private static class Lexer {

    private static final String LEFT_PARENT = "(";
    private static final String RIGHT_PARENT = ")";
    private static final String COMMA = ",";
    private static final char A_LOWER = 'a';
    private static final char Z_LOWER = 'z';
    private static final char A_UPPER = 'A';
    private static final char Z_UPPER = 'Z';
    private static final char ZERO = '0';
    private static final char NINE = '9';
    private static final char DOT = '.';
    private static final char MINUS = '-';
    private static final char PLUS = '+';
    private String source;
    private Token current;
    private String value;
    private int pointer = 0;

    private Lexer(String source) {
      this.source = source;
    }

    private Token getCurrentToken() {
      return current;
    }

    private String getCurrentTokenValue() {
      return value;
    }

    private boolean moveOn() {
      int start = pointer;
      char curChar;
      while (pointer < source.length()) {
        curChar = source.charAt(pointer);
        if (curChar == SPACE) {
          if (start == pointer++) {
            start++;
          } else {
            break;
          }
        } else if (isCharacterOrDigit(curChar) || curChar == DOT
                || curChar == WXUtils.PERCENT || curChar == MINUS || curChar == PLUS) {
          pointer++;
        } else {
          if (start == pointer) {
            pointer++;
          }
          break;
        }
      }
      if (start != pointer) {
        String symbol = source.substring(start, pointer);
        moveOn(symbol);
        return true;
      } else {
        current = null;
        value = null;
        return false;
      }
    }

    private void moveOn(String token) {
      if (LEFT_PARENT.equals(token)) {
        current = Token.LEFT_PARENT;
        value = LEFT_PARENT;
      } else if (RIGHT_PARENT.equals(token)) {
        current = Token.RIGHT_PARENT;
        value = RIGHT_PARENT;
      } else if (COMMA.equals(token)) {
        current = Token.COMMA;
        value = COMMA;
      } else if (isFuncName(token)) {
        current = Token.FUNC_NAME;
        value = token;
      } else {
        current = Token.PARAM_VALUE;
        value = token;
      }
    }

    private boolean isFuncName(CharSequence funcName) {
      char letter;
      for (int i = 0; i < funcName.length(); i++) {
        letter = funcName.charAt(i);
        if (!((A_LOWER <= letter && letter <= Z_LOWER) ||
                (A_UPPER <= letter && letter <= Z_UPPER) ||
                letter == MINUS)) {
          return false;
        }
      }
      return true;
    }

    private boolean isCharacterOrDigit(char letter) {
      return (ZERO <= letter && letter <= NINE) || (A_LOWER <= letter && letter <= Z_LOWER) ||
              (A_UPPER <= letter && letter <= Z_UPPER);
    }
  }

}
