/*
 * 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.drill.exec.expr.fn.impl;

import javax.inject.Inject;

import org.apache.drill.exec.expr.DrillSimpleFunc;
import org.apache.drill.exec.expr.annotations.FunctionTemplate;
import org.apache.drill.exec.expr.annotations.FunctionTemplate.NullHandling;
import org.apache.drill.exec.expr.annotations.Output;
import org.apache.drill.exec.expr.annotations.Param;
import org.apache.drill.exec.expr.annotations.Workspace;
import org.apache.drill.exec.expr.holders.BigIntHolder;
import org.apache.drill.exec.expr.holders.BitHolder;
import org.apache.drill.exec.expr.holders.DateHolder;
import org.apache.drill.exec.expr.holders.IntervalDayHolder;
import org.apache.drill.exec.expr.holders.IntervalHolder;
import org.apache.drill.exec.expr.holders.IntervalYearHolder;
import org.apache.drill.exec.expr.holders.NullableVarCharHolder;
import org.apache.drill.exec.expr.holders.TimeHolder;
import org.apache.drill.exec.expr.holders.TimeStampHolder;
import org.apache.drill.exec.expr.holders.VarCharHolder;
import org.apache.drill.exec.ops.ContextInformation;
import org.apache.drill.exec.physical.impl.project.OutputSizeEstimateConstants;

import io.netty.buffer.DrillBuf;

public class DateTypeFunctions {

  /**
   * Function to check if a varchar value can be cast to a date.
   *
   * At the time of writing this function, several other databases were checked
   * for behavior compatibility. There was not a consensus between oracle and Sql
   * server about the expected behavior of this function, and Postgres lacks it
   * completely.
   *
   * Sql Server appears to have both a DATEFORMAT and language locale setting that
   * can change the values accepted by this function. Oracle appears to support
   * several formats, some of which are not mentioned in the Sql Server docs. With
   * the lack of standardization, we decided to implement this function so that it
   * would only consider date strings that would be accepted by the cast function
   * as valid.
   */
  @SuppressWarnings("unused")
  @FunctionTemplate(name = "isdate", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.INTERNAL, costCategory = FunctionTemplate.FunctionCostCategory.COMPLEX)
  public static class IsDate implements DrillSimpleFunc {

    @Param
    NullableVarCharHolder in;
    @Output
    BitHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      // for a null input return false
      if (in.isSet == 0) {
        out.value = 0;
      } else {
        out.value = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers.isReadableAsDate(in.buffer,
            in.start, in.end) ? 1 : 0;
      }
    }
  }

  // Same as above, just for required input
  @SuppressWarnings("unused")
  @FunctionTemplate(name = "isdate", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.INTERNAL, costCategory = FunctionTemplate.FunctionCostCategory.COMPLEX)
  public static class IsDateRequiredInput implements DrillSimpleFunc {

    @Param
    VarCharHolder in;
    @Output
    BitHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      // for a null input return false
      out.value = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers.isReadableAsDate(in.buffer, in.start,
          in.end) ? 1 : 0;
    }
  }

  @FunctionTemplate(name = "intervaltype", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class IntervalType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputYears;
    @Param
    BigIntHolder inputMonths;
    @Param
    BigIntHolder inputDays;
    @Param
    BigIntHolder inputHours;
    @Param
    BigIntHolder inputMinutes;
    @Param
    BigIntHolder inputSeconds;
    @Param
    BigIntHolder inputMilliSeconds;
    @Output
    IntervalHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {

      out.months = (int) ((inputYears.value * org.apache.drill.exec.vector.DateUtilities.yearsToMonths)
          + (inputMonths.value));
      out.days = (int) inputDays.value;
      out.milliseconds = (int) ((inputHours.value * org.apache.drill.exec.vector.DateUtilities.hoursToMillis)
          + (inputMinutes.value * org.apache.drill.exec.vector.DateUtilities.minutesToMillis)
          + (inputSeconds.value * org.apache.drill.exec.vector.DateUtilities.secondsToMillis)
          + (inputMilliSeconds.value));
    }
  }

  @FunctionTemplate(name = "interval_year", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class IntervalYearType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputYears;
    @Param
    BigIntHolder inputMonths;
    @Output
    IntervalYearHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {

      out.value = (int) ((inputYears.value * org.apache.drill.exec.vector.DateUtilities.yearsToMonths)
          + (inputMonths.value));
    }
  }

  @FunctionTemplate(name = "interval_day", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class IntervalDayType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputDays;
    @Param
    BigIntHolder inputHours;
    @Param
    BigIntHolder inputMinutes;
    @Param
    BigIntHolder inputSeconds;
    @Param
    BigIntHolder inputMillis;
    @Output
    IntervalDayHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {

      out.days = (int) inputDays.value;
      out.milliseconds = (int) ((inputHours.value * org.apache.drill.exec.vector.DateUtilities.hoursToMillis)
          + (inputMinutes.value * org.apache.drill.exec.vector.DateUtilities.minutesToMillis)
          + (inputSeconds.value * org.apache.drill.exec.vector.DateUtilities.secondsToMillis)
          + (inputMillis.value));
    }
  }

  @FunctionTemplate(name = "datetype", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class DateType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputYears;
    @Param
    BigIntHolder inputMonths;
    @Param
    BigIntHolder inputDays;
    @Output
    DateHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = ((new org.joda.time.MutableDateTime((int) inputYears.value, (int) inputMonths.value,
          (int) inputDays.value, 0, 0, 0, 0, org.joda.time.DateTimeZone.UTC))).getMillis();
    }
  }

  @FunctionTemplate(name = "timestamptype", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class TimeStampType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputYears;
    @Param
    BigIntHolder inputMonths;
    @Param
    BigIntHolder inputDays;
    @Param
    BigIntHolder inputHours;
    @Param
    BigIntHolder inputMinutes;
    @Param
    BigIntHolder inputSeconds;
    @Param
    BigIntHolder inputMilliSeconds;
    @Output
    TimeStampHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = ((new org.joda.time.MutableDateTime((int) inputYears.value, (int) inputMonths.value,
          (int) inputDays.value, (int) inputHours.value, (int) inputMinutes.value, (int) inputSeconds.value,
          (int) inputMilliSeconds.value, org.joda.time.DateTimeZone.UTC))).getMillis();
    }
  }

  @FunctionTemplate(name = "timetype", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class TimeType implements DrillSimpleFunc {

    @Param
    BigIntHolder inputHours;
    @Param
    BigIntHolder inputMinutes;
    @Param
    BigIntHolder inputSeconds;
    @Param
    BigIntHolder inputMilliSeconds;
    @Output
    TimeHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = (int) ((inputHours.value * org.apache.drill.exec.vector.DateUtilities.hoursToMillis)
          + (inputMinutes.value * org.apache.drill.exec.vector.DateUtilities.minutesToMillis)
          + (inputSeconds.value * org.apache.drill.exec.vector.DateUtilities.secondsToMillis)
          + inputMilliSeconds.value);
    }
  }

  @FunctionTemplate(name = "current_date", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL, isNiladic = true)
  public static class CurrentDate implements DrillSimpleFunc {
    @Workspace
    long queryStartDate;
    @Output
    DateHolder out;
    @Inject
    ContextInformation contextInfo;

    @Override
    public void setup() {
      java.time.Instant queryStart = contextInfo.getQueryStartInstant();
      java.time.ZonedDateTime lzdt = queryStart.atZone(contextInfo.getRootFragmentTimeZone());
      java.time.LocalDate ld = lzdt.truncatedTo(java.time.temporal.ChronoUnit.DAYS).toLocalDate();
      queryStartDate = org.apache.drill.exec.vector.DateUtilities.toDrillDate(ld);
    }

    @Override
    public void eval() {
      out.value = queryStartDate;
    }

  }

  @FunctionTemplate(name = "timeofday", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL, isRandom = true, outputSizeEstimate = OutputSizeEstimateConstants.DATE_TIME_LENGTH)
  public static class TimeOfDay implements DrillSimpleFunc {
    @Inject
    DrillBuf buffer;
    @Output
    VarCharHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      java.time.ZonedDateTime temp = java.time.ZonedDateTime.now();
      String str = org.apache.drill.exec.expr.fn.impl.DateUtility.formatTimeStampTZ.format(temp);
      out.buffer = buffer;
      out.start = 0;
      out.end = Math.min(100, str.length()); // truncate if target type has length smaller than that of input's
                          // string
      out.buffer.setBytes(0, str.substring(0, out.end).getBytes(java.nio.charset.StandardCharsets.UTF_8));
    }
  }

  /*
   * Return query start time in milliseconds
   */
  public static long getQueryStartDate(ContextInformation contextInfo) {
    org.joda.time.DateTime now = (new org.joda.time.DateTime(contextInfo.getQueryStartTime()))
        .withZoneRetainFields(org.joda.time.DateTimeZone.UTC);
    return now.getMillis();
  }

  /*
   * Niladic version of LocalTimeStamp
   */
  @FunctionTemplate(names = { "localtimestamp",
      "current_timestamp" }, scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL, isNiladic = true)
  public static class LocalTimeStampNiladic implements DrillSimpleFunc {
    @Workspace
    long queryStartDate;
    @Output
    TimeStampHolder out;
    @Inject
    ContextInformation contextInfo;

    @Override
    public void setup() {
      queryStartDate = org.apache.drill.exec.expr.fn.impl.DateTypeFunctions.getQueryStartDate(contextInfo);
    }

    @Override
    public void eval() {
      out.value = queryStartDate;
    }
  }

  /*
   * Non-Niladic version of LocalTimeStamp
   */
  @FunctionTemplate(names = { "now", "statement_timestamp",
      "transaction_timestamp" }, scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class LocalTimeStampNonNiladic implements DrillSimpleFunc {
    @Workspace
    long queryStartDate;
    @Output
    TimeStampHolder out;
    @Inject
    ContextInformation contextInfo;

    @Override
    public void setup() {
      queryStartDate = org.apache.drill.exec.expr.fn.impl.DateTypeFunctions.getQueryStartDate(contextInfo);
    }

    @Override
    public void eval() {
      out.value = queryStartDate;
    }
  }

  @FunctionTemplate(names = { "current_time",
      "localtime" }, scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL, isNiladic = true)
  public static class CurrentTime implements DrillSimpleFunc {
    @Workspace
    int queryStartTime;
    @Output
    TimeHolder out;
    @Inject
    ContextInformation contextInfo;

    @Override
    public void setup() {
      java.time.Instant queryStart = contextInfo.getQueryStartInstant();
      java.time.ZonedDateTime lzdt = queryStart.atZone(contextInfo.getRootFragmentTimeZone());
      queryStartTime = org.apache.drill.exec.vector.DateUtilities.toDrillTime(lzdt.toLocalTime());
    }

    @Override
    public void eval() {
      out.value = queryStartTime;
    }
  }

  @SuppressWarnings("unused")
  @FunctionTemplate(names = { "date_add",
      "add" }, scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class DateTimeAddFunction implements DrillSimpleFunc {
    @Param
    DateHolder left;
    @Param
    TimeHolder right;
    @Output
    TimeStampHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = left.value + right.value;
    }
  }

  @SuppressWarnings("unused")
  @FunctionTemplate(names = { "date_add",
      "add" }, scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class TimeDateAddFunction implements DrillSimpleFunc {
    @Param
    TimeHolder right;
    @Param
    DateHolder left;
    @Output
    TimeStampHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = left.value + right.value;
    }
  }

  /*
   * Dummy function template to allow Optiq to validate this function call. At
   * DrillOptiq time we rewrite all date_part() functions to extract functions,
   * since they are essentially the same
   */
  @SuppressWarnings("unused")
  @FunctionTemplate(names = "date_part", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class DatePartFunction implements DrillSimpleFunc {
    @Param
    VarCharHolder left;
    @Param
    DateHolder right;
    @Output
    BigIntHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      // TODO: We should fix this so that the function is rewritten for the user rather than
      // throwing an exception. This is poor design.
      throw new UnsupportedOperationException(
        "date_part function should be rewritten as extract() functions");
    }
  }

  @FunctionTemplate(name = "castTIME", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class CastTimeStampToTime implements DrillSimpleFunc {
    @Param
    TimeStampHolder in;
    @Output
    TimeHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = (int) (in.value % org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis);
    }
  }

  @FunctionTemplate(name = "castTIMESTAMP", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class CastTimeToTimeStamp implements DrillSimpleFunc {
    @Param
    TimeHolder in;
    @Output
    TimeStampHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = in.value;
    }
  }

  @FunctionTemplate(name = "castTIME", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class CastDateToTime implements DrillSimpleFunc {
    @Param
    DateHolder in;
    @Output
    TimeHolder out;

    @Override
    public void setup() {
    }

    @Override
    public void eval() {
      out.value = 0;
    }
  }

  @FunctionTemplate(name = "unix_timestamp", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class UnixTimeStamp implements DrillSimpleFunc {
    @Output
    BigIntHolder out;
    @Workspace
    long queryStartDate;
    @Inject
    ContextInformation contextInfo;

    @Override
    public void setup() {
      queryStartDate = contextInfo.getQueryStartTime();
    }

    @Override
    public void eval() {
      out.value = queryStartDate / 1000;
    }
  }

  @FunctionTemplate(name = "unix_timestamp", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class UnixTimeStampForDate implements DrillSimpleFunc {
    @Param
    VarCharHolder inputDateValue;
    @Output
    BigIntHolder out;
    @Workspace
    org.joda.time.DateTime date;
    @Workspace
    org.joda.time.format.DateTimeFormatter formatter;

    @Override
    public void setup() {
      formatter = org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");
    }

    @Override
    public void eval() {
      String inputDate = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers
          .toStringFromUTF8(inputDateValue.start, inputDateValue.end, inputDateValue.buffer);
      date = formatter.parseDateTime(inputDate);
      out.value = date.getMillis() / 1000;
    }
  }

  @FunctionTemplate(name = "unix_timestamp", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
  public static class UnixTimeStampForDateWithPattern implements DrillSimpleFunc {
    @Param
    VarCharHolder inputDateValue;
    @Param
    VarCharHolder inputPattern;
    @Output
    BigIntHolder out;
    @Workspace
    org.joda.time.DateTime date;
    @Workspace
    org.joda.time.format.DateTimeFormatter formatter;

    @Override
    public void setup() {
      String pattern = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers
          .toStringFromUTF8(inputPattern.start, inputPattern.end, inputPattern.buffer);
      formatter = org.joda.time.format.DateTimeFormat.forPattern(pattern);
    }

    @Override
    public void eval() {
      String inputDate = org.apache.drill.exec.expr.fn.impl.StringFunctionHelpers
          .toStringFromUTF8(inputDateValue.start, inputDateValue.end, inputDateValue.buffer);
      date = formatter.parseDateTime(inputDate);
      out.value = date.getMillis() / 1000;
    }
  }
}
