/*
 * 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() {

            int timeZoneIndex = contextInfo.getRootFragmentTimeZone();
            org.joda.time.DateTimeZone timeZone = org.joda.time.DateTimeZone.forID(org.apache.drill.exec.expr.fn.impl.DateUtility.getTimeZone(timeZoneIndex));
            org.joda.time.DateTime now = new org.joda.time.DateTime(contextInfo.getQueryStartTime(), timeZone);
            queryStartDate = (new org.joda.time.DateMidnight(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), timeZone)).
                withZoneRetainFields(org.joda.time.DateTimeZone.UTC).getMillis();
        }

        @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());
        }
    }

    /*
     * 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() {

            int timeZoneIndex = contextInfo.getRootFragmentTimeZone();
            org.joda.time.DateTimeZone timeZone = org.joda.time.DateTimeZone.forID(org.apache.drill.exec.expr.fn.impl.DateUtility.getTimeZone(timeZoneIndex));
            org.joda.time.DateTime now = new org.joda.time.DateTime(contextInfo.getQueryStartTime(), timeZone);
            queryStartTime= (now.getHourOfDay() * org.apache.drill.exec.vector.DateUtilities.hoursToMillis) +
                                   (now.getMinuteOfHour() * org.apache.drill.exec.vector.DateUtilities.minutesToMillis) +
                                   (now.getSecondOfMinute() * org.apache.drill.exec.vector.DateUtilities.secondsToMillis) +
                                   (now.getMillisOfSecond());
        }

        @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() {
            if (1 == 1) {
                throw new UnsupportedOperationException("date_part function should be rewritten as extract() functions");
            }
        }
    }

    @FunctionTemplate(name = "age", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
    public static class AgeTimeStampFunction implements DrillSimpleFunc {
        @Param TimeStampHolder left;
        @Param TimeStampHolder right;
        @Output IntervalHolder out;

        @Override
        public void setup() {
        }

        @Override
        public void eval() {
            long diff = left.value - right.value;
            long days = diff / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis;
            out.months = (int) (days / org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.days = (int) (days % org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.milliseconds = (int) (diff % org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis);
        }
    }

    @FunctionTemplate(name = "age", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
    public static class AgeTimeStamp2Function implements DrillSimpleFunc {
        @Param TimeStampHolder right;
        @Workspace long queryStartDate;
        @Output IntervalHolder out;
        @Inject ContextInformation contextInfo;

        @Override
        public void setup() {
            int timeZoneIndex = contextInfo.getRootFragmentTimeZone();
            org.joda.time.DateTimeZone timeZone = org.joda.time.DateTimeZone.forID(org.apache.drill.exec.expr.fn.impl.DateUtility.getTimeZone(timeZoneIndex));
            org.joda.time.DateTime now = new org.joda.time.DateTime(contextInfo.getQueryStartTime(), timeZone);
            queryStartDate = (new org.joda.time.DateMidnight(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), timeZone)).getMillis();
        }

        @Override
        public void eval() {
            long diff = queryStartDate - right.value;
            long days = diff / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis;
            out.months = (int) (days / org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.days = (int) (days % org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.milliseconds = (int) (diff % org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis);
        }
    }

    @FunctionTemplate(name = "age", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
    public static class AgeDateFunction implements DrillSimpleFunc {
        @Param DateHolder left;
        @Param DateHolder right;
        @Output IntervalHolder out;

        @Override
        public void setup() {
        }

        @Override
        public void eval() {
          long diff = left.value - right.value;
          long days = diff / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis;
          out.months = (int) (days / org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
          out.days = (int) (days % org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
          out.milliseconds = (int) (diff % org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis);
        }
    }

    @FunctionTemplate(name = "age", scope = FunctionTemplate.FunctionScope.SIMPLE, nulls = NullHandling.NULL_IF_NULL)
    public static class AgeDate2Function implements DrillSimpleFunc {
        @Param DateHolder right;
        @Workspace long queryStartDate;
        @Output IntervalHolder out;
        @Inject ContextInformation contextInfo;

        @Override
        public void setup() {
            int timeZoneIndex = contextInfo.getRootFragmentTimeZone();
            org.joda.time.DateTimeZone timeZone = org.joda.time.DateTimeZone.forID(org.apache.drill.exec.expr.fn.impl.DateUtility.getTimeZone(timeZoneIndex));
            org.joda.time.DateTime now = new org.joda.time.DateTime(contextInfo.getQueryStartTime(), timeZone);
            queryStartDate = (new org.joda.time.DateMidnight(now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), timeZone)).getMillis();
        }

        @Override
        public void eval() {
            long diff = queryStartDate - right.value;
            long days = diff / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis;
            out.months = (int) (days / org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.days = (int) (days % org.apache.drill.exec.vector.DateUtilities.monthToStandardDays);
            out.milliseconds = (int) (diff % org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis);
        }
    }

    @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 = "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;
      }
    }
}
