/*
 * Copyright 2015 LinkedIn Corp.
 *
 * Licensed 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.
 */

package com.linkedin.featurefu.expr;

import java.util.ArrayList;
import java.util.List;
import java.util.MissingFormatArgumentException;
import java.util.Random;


/**
 *
 * Operator contains the actually computation logic
 * Can be easily extended to support new operators
 *
 *  Author: Leo Tang <http://www.linkedin.com/in/lijuntang>
 */
public abstract class Operator {

  private static final long DEFAULT_RANDOM_SEED = 0L;
  private static final double LN_2 = Math.log(2);

  /**
   * Calculate the value with this operator and its operands
   * @param operands List of Expr
   * @return value
   */
  public abstract double calculate(List<Expr> operands);

  /**
   * Number of operands required for this operator
   * used for parsing and sanity check purpose
   * @return The number of operands expected
   */
  public abstract int numberOfOperands();

  /**
   * The symbol to be used in the expression String, such as + - * / ==
   *  We don't use reflection to reuse a operator's class name, as most math operators have characters cannot be used in java class name
   * @return String representation of the operator
   */
  public abstract String getSymbol();

  /**
   * Parse an expr given operator string and operands string, the reason it's delegated here is because
   *  an operator knows how many operands it need
   * @param operator
   * @param operands
   * @param variableRegistry
   * @return expr parsed
   */
  public static Expr parse(String operator, List<String> operands, VariableRegistry variableRegistry) {
    Operator op;

    if (operator.equals("-") && operands.size() == 1) { //"-" could be subtract or unary minus
      op = Operator.UNARY_MINUS;
    } else {

      if (!OperatorsSupported.isSupported(operator)) {
        throw new UnsupportedOperationException(
            "Operator not supported: " + operator + ", the list of supported operators are: " + OperatorsSupported
                .getSupported());
      }

      op = OperatorsSupported.getOperator(operator);
    }

    return new Expression(op, op.parseOperands(operands, variableRegistry));
  }

  /**
   * Use polymorphism here, this is actually being called by Operator implementations, where they know their number of operands
   * @param operands    operands in list of strings
   * @param variableRegistry  registry for registering possible variables found in operands
   * @return operands in List of Expr
   */
  protected List<Expr> parseOperands(List<String> operands, VariableRegistry variableRegistry) {
    ArrayList<Expr> list = new ArrayList<Expr>();

    final int numOperands = this.numberOfOperands();

    if (operands.size() != numOperands) {
      throw new MissingFormatArgumentException(
          this.getSymbol() + " expect " + numOperands + " operands, actual number of operands is: " + operands.size());
    }

    for (int i = 0; i < numOperands; i++) {
      list.add(Expression.parse(operands.get(i), variableRegistry));
    }

    return list;
  }

  /**
   * Display the operator by its symbiol
   * @return  String representation of the operator
   */
  public String toString() {
    return this.getSymbol();
  }

  /**
   *  Implementations of an array of operators, add yours below and register it in class OperatorsSupported, just that simple
   *    Most of the implementations below are self explanatory, just to mention boolean is represented by double too:
   *       any double can be regarded as boolean, with implicit converter: double -> boolean:  0.0 is false, otherwise true
   *    Also 'if' is defined as a ternary operator (if x a b), equivalent to x ? a : b , where x, a, b can be any expressions
   */
  public static final Operator EQ = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() == operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return "==";
    }
  };

  public static final Operator NE = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() != operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return "!=";
    }
  };

  public static final Operator GT = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() > operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return ">";
    }
  };

  public static final Operator GT_EQ = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() >= operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return ">=";
    }
  };

  public static final Operator LT = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() < operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return "<";
    }
  };

  public static final Operator LT_EQ = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() <= operands.get(1).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return "<=";
    }
  };

  public static final Operator AND = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() != 0 && operands.get(1).evaluate() != 0 ? 1 : 0;
    }

    public String getSymbol() {
      return "&&";
    }
  };

  public static final Operator OR = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() != 0 || operands.get(1).evaluate() != 0 ? 1 : 0;
    }

    public String getSymbol() {
      return "||";
    }
  };

  public static final Operator ADD = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() + operands.get(1).evaluate();
    }

    public String getSymbol() {
      return "+";
    }
  };

  public static final Operator SUBTRACT = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() - operands.get(1).evaluate();
    }

    public String getSymbol() {
      return "-";
    }
  };

  public static final Operator MULTIPLY = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() * operands.get(1).evaluate();
    }

    public String getSymbol() {
      return "*";
    }
  };

  public static final Operator DIVIDE = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() / operands.get(1).evaluate();
    }

    public String getSymbol() {
      return "/";
    }
  };

  public static final Operator POWER = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return Math.pow(operands.get(0).evaluate(), operands.get(1).evaluate());
    }

    public String getSymbol() {
      return "**";
    }
  };

  public static final Operator LN = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.log(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "ln";
    }
  };

  public static final Operator LN1PLUS = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.log(1 + operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "ln1plus";
    }
  };

  public static final Operator LOG2 = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.log(operands.get(0).evaluate()) / LN_2;
    }

    public String getSymbol() {
      return "log2";
    }
  };

  public static final Operator MAX = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return Math.max(operands.get(0).evaluate(), operands.get(1).evaluate());
    }

    public String getSymbol() {
      return "max";
    }
  };

  public static final Operator MIN = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return Math.min(operands.get(0).evaluate(), operands.get(1).evaluate());
    }

    public String getSymbol() {
      return "min";
    }
  };

  public static final Operator ABS = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.abs(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "abs";
    }
  };

  public static final Operator UNARY_MINUS = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return -operands.get(0).evaluate();
    }

    public String getSymbol() {
      return "unaryMinus";
    }
  };

  public static final Operator MOD = new Operator() {

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() % operands.get(1).evaluate();
    }

    public String getSymbol() {
      return "%";
    }
  };

  public static final Operator IF = new Operator() {

    public int numberOfOperands() {
      return 3;
    }

    public double calculate(List<Expr> operands) {

      double check = operands.get(0).evaluate();

      return check != 0 ? operands.get(1).evaluate() : operands.get(2).evaluate();
    }

    public String getSymbol() {
      return "if";
    }
  };

  /**
   * We use a fixed seed here so that we can get deterministic unit test results
   * It will generate predictable random numbers which should be fine in most use cases,
   */
  public static final Operator RAND = new Operator() {

    private Random _generator = new Random(DEFAULT_RANDOM_SEED);

    public int numberOfOperands() {
      return 0;
    }

    public double calculate(List<Expr> operands) {
      return _generator.nextDouble();
    }

    public String getSymbol() {
      return "rand";
    }
  };

  /**
   * We use a fixed seed here so that we can get deterministic unit test results
   * It will generate predictable random numbers which should be fine in most use cases
   */
  public static final Operator RANDIN = new Operator() {

    private Random _generator = new Random(DEFAULT_RANDOM_SEED);

    public int numberOfOperands() {
      return 2;
    }

    public double calculate(List<Expr> operands) {
      double a = operands.get(0).evaluate();
      double b = operands.get(1).evaluate() - a;
      return a + b * _generator.nextDouble();
    }

    public String getSymbol() {
      return "rand-in";
    }
  };

  public static final Operator SIGN = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.signum(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "sign";
    }
  };

  public static final Operator EXP = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.exp(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "exp";
    }
  };

  public static final Operator SIGMOID = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return 1.0 / (1 + Math.exp(-operands.get(0).evaluate()));
    }

    public String getSymbol() {
      return "sigmoid";
    }
  };

  public static final Operator ROUND = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.round(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "round";
    }
  };

  public static final Operator FLOOR = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.floor(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "floor";
    }
  };

  public static final Operator CEIL = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.ceil(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "ceil";
    }
  };

  public static final Operator SQRT = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return Math.sqrt(operands.get(0).evaluate());
    }

    public String getSymbol() {
      return "sqrt";
    }
  };

  public static final Operator NOT = new Operator() {

    public int numberOfOperands() {
      return 1;
    }

    public double calculate(List<Expr> operands) {
      return operands.get(0).evaluate() != 0 ? 0 : 1;
    }

    public String getSymbol() {
      return "!";
    }
  };

  /**
   * (in x a b) checks if x is within half-open interval [a,b)
   */
  public static final Operator IN = new Operator() {

    public int numberOfOperands() {
      return 3;
    }

    public double calculate(List<Expr> operands) {
      double check = operands.get(0).evaluate();

      return check >= operands.get(1).evaluate() && check < operands.get(2).evaluate() ? 1 : 0;
    }

    public String getSymbol() {
      return "in";
    }
  };

  public static final Operator COS = new Operator() {
    public int numberOfOperands() {
        return 1;
    }

    public double calculate(List<Expr> operands) {
        return Math.cos(operands.get(0).evaluate());
    }

    public String getSymbol() {
        return "cos";
    }
  };


    public static final Operator SIN = new Operator() {
        public int numberOfOperands() {
            return 1;
        }

        public double calculate(List<Expr> operands) {
            return Math.sin(operands.get(0).evaluate());
        }

        public String getSymbol() {
            return "sin";
        }
    };

    public static final Operator TAN = new Operator() {
        public int numberOfOperands() {
            return 1;
        }

        public double calculate(List<Expr> operands) {
            return Math.tan(operands.get(0).evaluate());
        }

        public String getSymbol() {
            return "tan";
        }
    };

    public static final Operator TANH = new Operator() {
        public int numberOfOperands() {
            return 1;
        }

        public double calculate(List<Expr> operands) {
            return Math.tanh(operands.get(0).evaluate());
        }

        public String getSymbol() {
            return "tanh";
        }
    };
}