/*
 * 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.carbondata.core.scan.expression;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import org.apache.carbondata.core.constants.CarbonCommonConstants;
import org.apache.carbondata.core.keygenerator.directdictionary.timestamp.DateDirectDictionaryGenerator;
import org.apache.carbondata.core.metadata.datatype.DataType;
import org.apache.carbondata.core.metadata.datatype.DataTypes;
import org.apache.carbondata.core.scan.expression.exception.FilterIllegalMemberException;
import org.apache.carbondata.core.util.CarbonUtil;

public class ExpressionResult implements Comparable<ExpressionResult> {

  protected DataType dataType;

  protected Object value;

  private List<ExpressionResult> expressionResults;

  private boolean isLiteral = false;

  public ExpressionResult(DataType dataType, Object value) {
    this.dataType = dataType;
    this.value = value;
  }

  public ExpressionResult(DataType dataType, Object value, boolean isLiteral) {
    this(dataType, value);
    this.isLiteral = isLiteral;
  }

  public ExpressionResult(List<ExpressionResult> expressionResults) {
    this.expressionResults = expressionResults;
  }

  public void set(DataType dataType, Object value) {
    this.dataType = dataType;
    this.value = value;
    this.expressionResults = null;
  }

  public DataType getDataType() {
    return dataType;
  }

  public Integer getInt() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return Integer.parseInt(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.SHORT) {
        return ((Short) value).intValue();
      } else if (dataType == DataTypes.INT ||
          dataType == DataTypes.DOUBLE) {
        if (value instanceof Double) {
          return ((Double) value).intValue();
        }
        if (value instanceof Long) {
          return ((Long) value).intValue();
        }
        return (Integer) value;
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return (int) (((java.sql.Date) value).getTime());
        } else {
          return (Integer) value;
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return (int) (((Timestamp) value).getTime());
        } else {
          if (isLiteral) {
            Long l = (Long) value / 1000;
            return l.intValue();
          }
          return (Integer) value;
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to integer type value");
      }

    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Integer type value");
    }
  }

  public Short getShort() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return Short.parseShort(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.SHORT ||
          dataType == DataTypes.INT ||
          dataType == DataTypes.DOUBLE) {
        if (value instanceof Double) {
          return ((Double) value).shortValue();
        } else if (value instanceof Integer) {
          return ((Integer) value).shortValue();
        }
        return (Short) value;
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return (short) (((java.sql.Date) value).getTime());
        } else {
          return (Short) value;
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return (short) (((Timestamp) value).getTime());
        } else {
          if (isLiteral) {
            Long l = ((long) value / 1000);
            return l.shortValue();
          }
          return (Short) value;
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to integer type value");
      }

    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Integer type value");
    }
  }

  public String getString() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.DATE || dataType == DataTypes.TIMESTAMP) {
        String format = CarbonUtil.getFormatFromProperty(this.getDataType());
        SimpleDateFormat parser = new SimpleDateFormat(format);
        if (this.getDataType() == DataTypes.DATE) {
          parser.setTimeZone(TimeZone.getTimeZone("GMT"));
        }
        if (value instanceof Timestamp) {
          return parser.format((Timestamp) value);
        } else if (value instanceof java.sql.Date) {
          return parser.format((java.sql.Date) value);
        } else if (value instanceof Long) {
          if (isLiteral) {
            return parser.format(new Timestamp((long) value / 1000));
          }
          return parser.format(new Timestamp((long) value));
        } else if (value instanceof Integer) {
          long date = ((int) value) * DateDirectDictionaryGenerator.MILLIS_PER_DAY;
          return parser.format(new java.sql.Date(date));
        }
        return value.toString();
      } else {
        return value.toString();
      }
    } catch (Exception e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to String type value");
    }
  }

  public Double getDouble() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return Double.parseDouble(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.SHORT) {
        return ((Short) value).doubleValue();
      } else if (dataType == DataTypes.INT) {
        return ((Integer) value).doubleValue();
      } else if (dataType == DataTypes.LONG) {
        return ((Long) value).doubleValue();
      } else if (dataType == DataTypes.DOUBLE) {
        return (Double) value;
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return (double) ((java.sql.Date) value).getTime();
        } else {
          return (Double) (value);
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return (double) ((Timestamp) value).getTime();
        } else {
          if (isLiteral) {
            Long l = (Long) value / 1000;
            return l.doubleValue();
          }
          return (Double) (value);
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to double type value");
      }
    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Double type value");
    }
  }

  public Long getLong() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return Long.parseLong(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.SHORT) {
        return ((Short) value).longValue();
      } else if (dataType == DataTypes.INT) {
        return (Long) value;
      } else if (dataType == DataTypes.LONG) {
        return (Long) value;
      } else if (dataType == DataTypes.DOUBLE) {
        return (Long) value;
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return ((java.sql.Date) value).getTime();
        } else {
          return (Long) value;
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return ((Timestamp) value).getTime();
        } else {
          return (Long) value;
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to Long type value");
      }
    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(e.getMessage() + " " +
          "Cannot convert" + this.getDataType().getName() + " to Long type value");
    }

  }

  //Add to judge for BigDecimal
  public BigDecimal getDecimal() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return new BigDecimal(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.SHORT) {
        return new BigDecimal((short) value);
      } else if (dataType == DataTypes.INT) {
        return new BigDecimal((int) value);
      } else if (dataType == DataTypes.LONG) {
        return new BigDecimal((long) value);
      } else if (dataType == DataTypes.DOUBLE || DataTypes.isDecimal(dataType)) {
        return new BigDecimal(value.toString());
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return new BigDecimal(((java.sql.Date) value).getTime());
        } else {
          return new BigDecimal((long) value);
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return new BigDecimal(((Timestamp) value).getTime());
        } else {
          if (isLiteral) {
            return new BigDecimal((long) value / 1000);
          }
          return new BigDecimal((long) value);
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to Decimal type value");
      }
    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Decimal type value");
    }

  }

  public Long getTime() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        // Currently the query engine layer only supports yyyy-MM-dd HH:mm:ss date format
        // no matter in which format the data is been stored, so while retrieving the direct
        // surrogate value for filter member first it should be converted in date form as per
        // above format and needs to retrieve time stamp.
        SimpleDateFormat parser =
            new SimpleDateFormat(CarbonCommonConstants.CARBON_TIMESTAMP_DEFAULT_FORMAT);
        Date dateToStr;
        try {
          dateToStr = parser.parse(value.toString());
          return dateToStr.getTime();
        } catch (ParseException e) {
          throw new FilterIllegalMemberException(
              "Cannot convert" + this.getDataType().getName() + " to Time type value");
        }
      } else if (dataType == DataTypes.SHORT) {
        return ((Short) value).longValue();
      } else if (dataType == DataTypes.INT || dataType == DataTypes.LONG) {
        return (Long) value;
      } else if (dataType == DataTypes.DOUBLE) {
        return (Long) value;
      } else if (dataType == DataTypes.DATE) {
        if (value instanceof java.sql.Date) {
          return ((Date) value).getTime();
        } else {
          return (Long) value;
        }
      } else if (dataType == DataTypes.TIMESTAMP) {
        if (value instanceof Timestamp) {
          return ((Timestamp) value).getTime();
        } else {
          if (isLiteral) {
            return (Long) value / 1000;
          }
          return (Long) value;
        }
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to Time type value");
      }
    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Time type value");
    }

  }

  public Boolean getBoolean() throws FilterIllegalMemberException {
    if (value == null) {
      return null;
    }
    try {
      DataType dataType = this.getDataType();
      if (dataType == DataTypes.STRING) {
        try {
          return Boolean.parseBoolean(value.toString());
        } catch (NumberFormatException e) {
          throw new FilterIllegalMemberException(e);
        }
      } else if (dataType == DataTypes.BOOLEAN) {
        return Boolean.parseBoolean(value.toString());
      } else {
        throw new FilterIllegalMemberException(
            "Cannot convert" + this.getDataType().getName() + " to boolean type value");
      }
    } catch (ClassCastException e) {
      throw new FilterIllegalMemberException(
          "Cannot convert" + this.getDataType().getName() + " to Boolean type value");
    }
  }

  public List<ExpressionResult> getList() {
    if (null == expressionResults) {
      List<ExpressionResult> a = new ArrayList<ExpressionResult>(20);
      a.add(new ExpressionResult(dataType, value, isLiteral));
      return a;
    } else {
      return expressionResults;
    }
  }

  public List<String> getListAsString() throws FilterIllegalMemberException {
    List<String> evaluateResultListFinal = new ArrayList<String>(20);
    List<ExpressionResult> evaluateResultList = getList();
    for (ExpressionResult result : evaluateResultList) {
      String resultString = result.getString();
      if (resultString == null) {
        evaluateResultListFinal.add(CarbonCommonConstants.MEMBER_DEFAULT_VAL);
        continue;
      }
      evaluateResultListFinal.add(resultString);
    }
    return evaluateResultListFinal;
  }

  @Override public int hashCode() {
    final int prime = 31;
    int result = 1;
    if (null != expressionResults) {
      result = prime * result + expressionResults.hashCode();
    } else if (null != value) {
      result = prime * result + value.toString().hashCode();
    } else {
      result = prime * result + "".hashCode();
    }

    return result;
  }

  @Override public boolean equals(Object obj) {
    if (!(obj instanceof ExpressionResult)) {
      return false;
    }
    if (this == obj) {
      return true;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    ExpressionResult objToCompare = (ExpressionResult) obj;
    boolean result = false;
    if (this.value == objToCompare.value) {
      return true;
    }

    if (this.isNull() || objToCompare.isNull()) {
      return false;
    }

    // make the comparison against the data type whose precedence is higher like
    // LONG precedence is higher than INT, so from int value we should get the long value
    // and then compare both the values. If done vice versa exception will be thrown
    // and comparison will fail
    DataType dataType = null;
    if (objToCompare.getDataType().getPrecedenceOrder() < this.getDataType().getPrecedenceOrder()) {
      dataType = this.getDataType();
    } else {
      dataType = objToCompare.getDataType();
    }
    try {
      if (dataType == DataTypes.STRING) {
        result = this.getString().equals(objToCompare.getString());
      } else if (dataType == DataTypes.SHORT) {
        result = this.getShort().equals(objToCompare.getShort());
      } else if (dataType == DataTypes.INT) {
        result = this.getInt().equals(objToCompare.getInt());
      } else if (dataType == DataTypes.LONG ||
          dataType == DataTypes.DATE ||
          dataType == DataTypes.TIMESTAMP) {
        result = this.getLong().equals(objToCompare.getLong());
      } else if (dataType == DataTypes.DOUBLE) {
        result = this.getDouble().equals(objToCompare.getDouble());
      } else if (DataTypes.isDecimal(dataType)) {
        result = this.getDecimal().equals(objToCompare.getDecimal());
      }
    } catch (FilterIllegalMemberException ex) {
      return false;
    }

    return result;
  }

  public boolean isNull() {
    return value == null;
  }

  @Override public int compareTo(ExpressionResult o) {
    try {
      DataType type = o.dataType;
      if (type == DataTypes.SHORT ||
          type == DataTypes.INT ||
          type == DataTypes.LONG ||
          type == DataTypes.DOUBLE) {
        Double d1 = this.getDouble();
        Double d2 = o.getDouble();
        return d1.compareTo(d2);
      } else if (DataTypes.isDecimal(type)) {
        java.math.BigDecimal val1 = this.getDecimal();
        java.math.BigDecimal val2 = o.getDecimal();
        return val1.compareTo(val2);
      } else if (type == DataTypes.DATE || type == DataTypes.TIMESTAMP) {
        String format = CarbonUtil.getFormatFromProperty(o.dataType);
        SimpleDateFormat parser = new SimpleDateFormat(format);
        Date date1 = parser.parse(this.getString());
        Date date2 = parser.parse(o.getString());
        return date1.compareTo(date2);
      } else {
        return this.getString().compareTo(o.getString());
      }
    } catch (ParseException | FilterIllegalMemberException e) {
      return -1;
    }
  }

}
