/*
 * Copyright (c) 2004, PostgreSQL Global Development Group
 * See the LICENSE file in the project root for more information.
 */

package org.postgresql.jdbc;

import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * This class stores supported escaped function.
 *
 * @author Xavier Poinsard
 * @deprecated see {@link EscapedFunctions2}
 */
@Deprecated
public class EscapedFunctions {
  // numeric functions names
  public static final String ABS = "abs";
  public static final String ACOS = "acos";
  public static final String ASIN = "asin";
  public static final String ATAN = "atan";
  public static final String ATAN2 = "atan2";
  public static final String CEILING = "ceiling";
  public static final String COS = "cos";
  public static final String COT = "cot";
  public static final String DEGREES = "degrees";
  public static final String EXP = "exp";
  public static final String FLOOR = "floor";
  public static final String LOG = "log";
  public static final String LOG10 = "log10";
  public static final String MOD = "mod";
  public static final String PI = "pi";
  public static final String POWER = "power";
  public static final String RADIANS = "radians";
  public static final String ROUND = "round";
  public static final String SIGN = "sign";
  public static final String SIN = "sin";
  public static final String SQRT = "sqrt";
  public static final String TAN = "tan";
  public static final String TRUNCATE = "truncate";

  // string function names
  public static final String ASCII = "ascii";
  public static final String CHAR = "char";
  public static final String CONCAT = "concat";
  public static final String INSERT = "insert"; // change arguments order
  public static final String LCASE = "lcase";
  public static final String LEFT = "left";
  public static final String LENGTH = "length";
  public static final String LOCATE = "locate"; // the 3 args version duplicate args
  public static final String LTRIM = "ltrim";
  public static final String REPEAT = "repeat";
  public static final String REPLACE = "replace";
  public static final String RIGHT = "right"; // duplicate args
  public static final String RTRIM = "rtrim";
  public static final String SPACE = "space";
  public static final String SUBSTRING = "substring";
  public static final String UCASE = "ucase";
  // soundex is implemented on the server side by
  // the contrib/fuzzystrmatch module. We provide a translation
  // for this in the driver, but since we don't want to bother with run
  // time detection of this module's installation we don't report this
  // method as supported in DatabaseMetaData.
  // difference is currently unsupported entirely.

  // date time function names
  public static final String CURDATE = "curdate";
  public static final String CURTIME = "curtime";
  public static final String DAYNAME = "dayname";
  public static final String DAYOFMONTH = "dayofmonth";
  public static final String DAYOFWEEK = "dayofweek";
  public static final String DAYOFYEAR = "dayofyear";
  public static final String HOUR = "hour";
  public static final String MINUTE = "minute";
  public static final String MONTH = "month";
  public static final String MONTHNAME = "monthname";
  public static final String NOW = "now";
  public static final String QUARTER = "quarter";
  public static final String SECOND = "second";
  public static final String WEEK = "week";
  public static final String YEAR = "year";
  // for timestampadd and timestampdiff the fractional part of second is not supported
  // by the backend
  // timestampdiff is very partially supported
  public static final String TIMESTAMPADD = "timestampadd";
  public static final String TIMESTAMPDIFF = "timestampdiff";

  // constants for timestampadd and timestampdiff
  public static final String SQL_TSI_ROOT = "SQL_TSI_";
  public static final String SQL_TSI_DAY = "DAY";
  public static final String SQL_TSI_FRAC_SECOND = "FRAC_SECOND";
  public static final String SQL_TSI_HOUR = "HOUR";
  public static final String SQL_TSI_MINUTE = "MINUTE";
  public static final String SQL_TSI_MONTH = "MONTH";
  public static final String SQL_TSI_QUARTER = "QUARTER";
  public static final String SQL_TSI_SECOND = "SECOND";
  public static final String SQL_TSI_WEEK = "WEEK";
  public static final String SQL_TSI_YEAR = "YEAR";


  // system functions
  public static final String DATABASE = "database";
  public static final String IFNULL = "ifnull";
  public static final String USER = "user";


  /**
   * storage for functions implementations.
   */
  private static Map<String, Method> functionMap = createFunctionMap();

  private static Map<String, Method> createFunctionMap() {
    Method[] arrayMeths = EscapedFunctions.class.getDeclaredMethods();
    Map<String, Method> functionHashMap = new HashMap<String, Method>(arrayMeths.length * 2);
    for (Method meth : arrayMeths) {
      if (meth.getName().startsWith("sql")) {
        functionHashMap.put(meth.getName().toLowerCase(Locale.US), meth);
      }
    }
    return functionHashMap;
  }

  /**
   * get Method object implementing the given function.
   *
   * @param functionName name of the searched function
   * @return a Method object or null if not found
   */
  public static Method getFunction(String functionName) {
    return functionMap.get("sql" + functionName.toLowerCase(Locale.US));
  }

  // ** numeric functions translations **

  /**
   * ceiling to ceil translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlceiling(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("ceil(", "ceiling", parsedArgs);
  }

  /**
   * log to ln translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqllog(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("ln(", "log", parsedArgs);
  }

  /**
   * log10 to log translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqllog10(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("log(", "log10", parsedArgs);
  }

  /**
   * power to pow translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlpower(List<?> parsedArgs) throws SQLException {
    return twoArgumentsFunctionCall("pow(", "power", parsedArgs);
  }

  /**
   * truncate to trunc translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqltruncate(List<?> parsedArgs) throws SQLException {
    return twoArgumentsFunctionCall("trunc(", "truncate", parsedArgs);
  }

  // ** string functions translations **

  /**
   * char to chr translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlchar(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("chr(", "char", parsedArgs);
  }

  /**
   * concat translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   */
  public static String sqlconcat(List<?> parsedArgs) {
    StringBuilder buf = new StringBuilder();
    buf.append('(');
    for (int iArg = 0; iArg < parsedArgs.size(); iArg++) {
      buf.append(parsedArgs.get(iArg));
      if (iArg != (parsedArgs.size() - 1)) {
        buf.append(" || ");
      }
    }
    return buf.append(')').toString();
  }

  /**
   * insert to overlay translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlinsert(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 4) {
      throw new PSQLException(GT.tr("{0} function takes four and only four argument.", "insert"),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append("overlay(");
    buf.append(parsedArgs.get(0)).append(" placing ").append(parsedArgs.get(3));
    buf.append(" from ").append(parsedArgs.get(1)).append(" for ").append(parsedArgs.get(2));
    return buf.append(')').toString();
  }

  /**
   * lcase to lower translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqllcase(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("lower(", "lcase", parsedArgs);
  }

  /**
   * left to substring translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlleft(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 2) {
      throw new PSQLException(GT.tr("{0} function takes two and only two arguments.", "left"),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append("substring(");
    buf.append(parsedArgs.get(0)).append(" for ").append(parsedArgs.get(1));
    return buf.append(')').toString();
  }

  /**
   * length translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqllength(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 1) {
      throw new PSQLException(GT.tr("{0} function takes one and only one argument.", "length"),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append("length(trim(trailing from ");
    buf.append(parsedArgs.get(0));
    return buf.append("))").toString();
  }

  /**
   * locate translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqllocate(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() == 2) {
      return "position(" + parsedArgs.get(0) + " in " + parsedArgs.get(1) + ")";
    } else if (parsedArgs.size() == 3) {
      String tmp = "position(" + parsedArgs.get(0) + " in substring(" + parsedArgs.get(1) + " from "
          + parsedArgs.get(2) + "))";
      return "(" + parsedArgs.get(2) + "*sign(" + tmp + ")+" + tmp + ")";
    } else {
      throw new PSQLException(GT.tr("{0} function takes two or three arguments.", "locate"),
          PSQLState.SYNTAX_ERROR);
    }
  }

  /**
   * ltrim translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlltrim(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("trim(leading from ", "ltrim", parsedArgs);
  }

  /**
   * right to substring translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlright(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 2) {
      throw new PSQLException(GT.tr("{0} function takes two and only two arguments.", "right"),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append("substring(");
    buf.append(parsedArgs.get(0))
        .append(" from (length(")
        .append(parsedArgs.get(0))
        .append(")+1-")
        .append(parsedArgs.get(1));
    return buf.append("))").toString();
  }

  /**
   * rtrim translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlrtrim(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("trim(trailing from ", "rtrim", parsedArgs);
  }

  /**
   * space translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlspace(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("repeat(' ',", "space", parsedArgs);
  }

  /**
   * substring to substr translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlsubstring(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() == 2) {
      return "substr(" + parsedArgs.get(0) + "," + parsedArgs.get(1) + ")";
    } else if (parsedArgs.size() == 3) {
      return "substr(" + parsedArgs.get(0) + "," + parsedArgs.get(1) + "," + parsedArgs.get(2)
          + ")";
    } else {
      throw new PSQLException(GT.tr("{0} function takes two or three arguments.", "substring"),
          PSQLState.SYNTAX_ERROR);
    }
  }

  /**
   * ucase to upper translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlucase(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("upper(", "ucase", parsedArgs);
  }

  /**
   * curdate to current_date translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlcurdate(List<?> parsedArgs) throws SQLException {
    if (!parsedArgs.isEmpty()) {
      throw new PSQLException(GT.tr("{0} function doesn''t take any argument.", "curdate"),
          PSQLState.SYNTAX_ERROR);
    }
    return "current_date";
  }

  /**
   * curtime to current_time translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlcurtime(List<?> parsedArgs) throws SQLException {
    if (!parsedArgs.isEmpty()) {
      throw new PSQLException(GT.tr("{0} function doesn''t take any argument.", "curtime"),
          PSQLState.SYNTAX_ERROR);
    }
    return "current_time";
  }

  /**
   * dayname translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqldayname(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 1) {
      throw new PSQLException(GT.tr("{0} function takes one and only one argument.", "dayname"),
          PSQLState.SYNTAX_ERROR);
    }
    return "to_char(" + parsedArgs.get(0) + ",'Day')";
  }

  /**
   * dayofmonth translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqldayofmonth(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(day from ", "dayofmonth", parsedArgs);
  }

  /**
   * dayofweek translation adding 1 to postgresql function since we expect values from 1 to 7.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqldayofweek(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 1) {
      throw new PSQLException(GT.tr("{0} function takes one and only one argument.", "dayofweek"),
          PSQLState.SYNTAX_ERROR);
    }
    return "extract(dow from " + parsedArgs.get(0) + ")+1";
  }

  /**
   * dayofyear translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqldayofyear(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(doy from ", "dayofyear", parsedArgs);
  }

  /**
   * hour translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlhour(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(hour from ", "hour", parsedArgs);
  }

  /**
   * minute translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlminute(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(minute from ", "minute", parsedArgs);
  }

  /**
   * month translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlmonth(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(month from ", "month", parsedArgs);
  }

  /**
   * monthname translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlmonthname(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 1) {
      throw new PSQLException(GT.tr("{0} function takes one and only one argument.", "monthname"),
          PSQLState.SYNTAX_ERROR);
    }
    return "to_char(" + parsedArgs.get(0) + ",'Month')";
  }

  /**
   * quarter translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlquarter(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(quarter from ", "quarter", parsedArgs);
  }

  /**
   * second translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlsecond(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(second from ", "second", parsedArgs);
  }

  /**
   * week translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlweek(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(week from ", "week", parsedArgs);
  }

  /**
   * year translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlyear(List<?> parsedArgs) throws SQLException {
    return singleArgumentFunctionCall("extract(year from ", "year", parsedArgs);
  }

  /**
   * time stamp add.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqltimestampadd(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 3) {
      throw new PSQLException(
          GT.tr("{0} function takes three and only three arguments.", "timestampadd"),
          PSQLState.SYNTAX_ERROR);
    }
    String interval = EscapedFunctions.constantToInterval(parsedArgs.get(0).toString(),
        parsedArgs.get(1).toString());
    StringBuilder buf = new StringBuilder();
    buf.append("(").append(interval).append("+");
    buf.append(parsedArgs.get(2)).append(")");
    return buf.toString();
  }

  private static String constantToInterval(String type, String value) throws SQLException {
    if (!type.startsWith(SQL_TSI_ROOT)) {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", type),
          PSQLState.SYNTAX_ERROR);
    }
    String shortType = type.substring(SQL_TSI_ROOT.length());
    if (SQL_TSI_DAY.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' day' as interval)";
    } else if (SQL_TSI_SECOND.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' second' as interval)";
    } else if (SQL_TSI_HOUR.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' hour' as interval)";
    } else if (SQL_TSI_MINUTE.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' minute' as interval)";
    } else if (SQL_TSI_MONTH.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' month' as interval)";
    } else if (SQL_TSI_QUARTER.equalsIgnoreCase(shortType)) {
      return "CAST((" + value + "::int * 3) || ' month' as interval)";
    } else if (SQL_TSI_WEEK.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' week' as interval)";
    } else if (SQL_TSI_YEAR.equalsIgnoreCase(shortType)) {
      return "CAST(" + value + " || ' year' as interval)";
    } else if (SQL_TSI_FRAC_SECOND.equalsIgnoreCase(shortType)) {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", "SQL_TSI_FRAC_SECOND"),
          PSQLState.SYNTAX_ERROR);
    } else {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", type),
          PSQLState.SYNTAX_ERROR);
    }
  }


  /**
   * time stamp diff.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqltimestampdiff(List<?> parsedArgs) throws SQLException {
    if (parsedArgs.size() != 3) {
      throw new PSQLException(
          GT.tr("{0} function takes three and only three arguments.", "timestampdiff"),
          PSQLState.SYNTAX_ERROR);
    }
    String datePart = EscapedFunctions.constantToDatePart(parsedArgs.get(0).toString());
    StringBuilder buf = new StringBuilder();
    buf.append("extract( ")
        .append(datePart)
        .append(" from (")
        .append(parsedArgs.get(2))
        .append("-")
        .append(parsedArgs.get(1))
        .append("))");
    return buf.toString();
  }

  private static String constantToDatePart(String type) throws SQLException {
    if (!type.startsWith(SQL_TSI_ROOT)) {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", type),
          PSQLState.SYNTAX_ERROR);
    }
    String shortType = type.substring(SQL_TSI_ROOT.length());
    if (SQL_TSI_DAY.equalsIgnoreCase(shortType)) {
      return "day";
    } else if (SQL_TSI_SECOND.equalsIgnoreCase(shortType)) {
      return "second";
    } else if (SQL_TSI_HOUR.equalsIgnoreCase(shortType)) {
      return "hour";
    } else if (SQL_TSI_MINUTE.equalsIgnoreCase(shortType)) {
      return "minute";
    } else if (SQL_TSI_FRAC_SECOND.equalsIgnoreCase(shortType)) {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", "SQL_TSI_FRAC_SECOND"),
          PSQLState.SYNTAX_ERROR);
    } else {
      throw new PSQLException(GT.tr("Interval {0} not yet implemented", type),
          PSQLState.SYNTAX_ERROR);
    }
  }

  /**
   * database translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqldatabase(List<?> parsedArgs) throws SQLException {
    if (!parsedArgs.isEmpty()) {
      throw new PSQLException(GT.tr("{0} function doesn''t take any argument.", "database"),
          PSQLState.SYNTAX_ERROR);
    }
    return "current_database()";
  }

  /**
   * ifnull translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqlifnull(List<?> parsedArgs) throws SQLException {
    return twoArgumentsFunctionCall("coalesce(", "ifnull", parsedArgs);
  }

  /**
   * user translation.
   *
   * @param parsedArgs arguments
   * @return sql call
   * @throws SQLException if something wrong happens
   */
  public static String sqluser(List<?> parsedArgs) throws SQLException {
    if (!parsedArgs.isEmpty()) {
      throw new PSQLException(GT.tr("{0} function doesn''t take any argument.", "user"),
          PSQLState.SYNTAX_ERROR);
    }
    return "user";
  }

  private static String singleArgumentFunctionCall(String call, String functionName,
      List<?> parsedArgs) throws PSQLException {
    if (parsedArgs.size() != 1) {
      throw new PSQLException(GT.tr("{0} function takes one and only one argument.", functionName),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append(call);
    buf.append(parsedArgs.get(0));
    return buf.append(')').toString();
  }

  private static String twoArgumentsFunctionCall(String call, String functionName,
      List<?> parsedArgs) throws PSQLException {
    if (parsedArgs.size() != 2) {
      throw new PSQLException(GT.tr("{0} function takes two and only two arguments.", functionName),
          PSQLState.SYNTAX_ERROR);
    }
    StringBuilder buf = new StringBuilder();
    buf.append(call);
    buf.append(parsedArgs.get(0)).append(',').append(parsedArgs.get(1));
    return buf.append(')').toString();
  }
}
