/*
 * Copyright (C) 2016-2022 ActionTech.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */

/**
 *
 */
package com.actiontech.dble.plan.common.item.function.timefunc;

import com.actiontech.dble.plan.common.field.Field;
import com.actiontech.dble.plan.common.item.FieldTypes;
import com.actiontech.dble.plan.common.item.Item;
import com.actiontech.dble.plan.common.ptr.StringPtr;
import com.actiontech.dble.plan.common.time.*;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.expr.SQLIntervalExpr;
import com.alibaba.druid.sql.ast.expr.SQLIntervalUnit;
import com.alibaba.druid.sql.ast.expr.SQLMethodInvokeExpr;

import java.util.ArrayList;
import java.util.List;


public class ItemDateAddInterval extends ItemTemporalHybridFunc {
    StringPtr strValue = new StringPtr("");
    private SQLIntervalUnit intType;
    private boolean dateSubInterval;

    public ItemDateAddInterval(Item a, Item b, SQLIntervalUnit type, boolean neg, int charsetIndex) {
        super(new ArrayList<Item>(), charsetIndex);
        args.add(a);
        args.add(b);
        this.intType = type;
        this.dateSubInterval = neg;
    }

    @Override
    public final String funcName() {
        return "DATE_ADD";
    }

    @Override
    public void fixLengthAndDec() {
        FieldTypes arg0FieldType;

        maybeNull = true;

        /*
         * The field type for the result of an Item_date function is defined as
         * follows:
         *
         * - If first arg is a MYSQL_TYPE_DATETIME result is MYSQL_TYPE_DATETIME
         * - If first arg is a MYSQL_TYPE_DATE and the interval type uses hours,
         * minutes or seconds then type is MYSQL_TYPE_DATETIME. - Otherwise the
         * result is MYSQL_TYPE_STRING (This is because you can't know if the
         * string contains a DATE, MYSQL_TIME or DATETIME argument)
         */
        arg0FieldType = args.get(0).fieldType();
        int intervalDec = 0;
        if (intType == SQLIntervalUnit.MICROSECOND || intType == SQLIntervalUnit.DAY_MICROSECOND ||
                intType == SQLIntervalUnit.HOUR_MICROSECOND || intType == SQLIntervalUnit.MINUTE_MICROSECOND ||
                intType == SQLIntervalUnit.SECOND_MICROSECOND)
            intervalDec = MyTime.DATETIME_MAX_DECIMALS;
        else if (intType == SQLIntervalUnit.SECOND && args.get(1).getDecimals() > 0)
            intervalDec = Math.min(args.get(1).getDecimals(), MyTime.DATETIME_MAX_DECIMALS);

        if (arg0FieldType == FieldTypes.MYSQL_TYPE_DATETIME ||
                arg0FieldType == FieldTypes.MYSQL_TYPE_TIMESTAMP) {
            int dec = Math.max(args.get(0).datetimePrecision(), intervalDec);
            fixLengthAndDecAndCharsetDatetime(MyTime.MAX_DATETIME_WIDTH, dec);
            cachedFieldType = FieldTypes.MYSQL_TYPE_DATETIME;
        } else if (arg0FieldType == FieldTypes.MYSQL_TYPE_DATE) {
            if (intType == SQLIntervalUnit.YEAR || intType == SQLIntervalUnit.QUARTER ||
                    intType == SQLIntervalUnit.MONTH || intType == SQLIntervalUnit.WEEK ||
                    intType == SQLIntervalUnit.DAY || intType == SQLIntervalUnit.YEAR_MONTH) {
                cachedFieldType = FieldTypes.MYSQL_TYPE_DATE;
                fixLengthAndDecAndCharsetDatetime(MyTime.MAX_DATE_WIDTH, 0);
            } else {
                cachedFieldType = FieldTypes.MYSQL_TYPE_DATETIME;
                fixLengthAndDecAndCharsetDatetime(MyTime.MAX_DATE_WIDTH, intervalDec);
            }
        } else if (arg0FieldType == FieldTypes.MYSQL_TYPE_TIME) {
            int dec = Math.max(args.get(0).timePrecision(), intervalDec);
            cachedFieldType = FieldTypes.MYSQL_TYPE_TIME;
            fixLengthAndDecAndCharsetDatetime(MyTime.MAX_TIME_WIDTH, dec);
        } else {
            cachedFieldType = FieldTypes.MYSQL_TYPE_STRING;
            /* Behave as a usual string function when return type is VARCHAR. */
            // fix_length_and_charset(MyTime.MAX_DATETIME_FULL_WIDTH);
        }
    }

    /* Here arg[1] is a Item_interval object */
    private boolean getDateInternal(MySQLTime ltime, long fuzzyDate) {
        Interval interval = new Interval();

        if (args.get(0).getDate(ltime, MyTime.TIME_NO_ZERO_DATE) ||
                MyTime.getIntervalValue(args.get(1), intType, strValue, interval))
            return (nullValue = true);

        if (dateSubInterval)
            interval.setNeg(!interval.isNeg());

        /*
         * Make sure we return proper time_type. It's important for val_str().
         */
        if (cachedFieldType == FieldTypes.MYSQL_TYPE_DATE &&
                ltime.getTimeType() == MySQLTimestampType.MYSQL_TIMESTAMP_DATETIME)
            MyTime.datetimeToDate(ltime);
        else if (cachedFieldType == FieldTypes.MYSQL_TYPE_DATETIME &&
                ltime.getTimeType() == MySQLTimestampType.MYSQL_TIMESTAMP_DATE)
            MyTime.dateToDatetime(ltime);

        return (nullValue = MyTime.dateAddInterval(ltime, intType, interval));
    }

    private boolean getTimeInternal(MySQLTime ltime) {
        Interval interval = new Interval();
        if ((nullValue = args.get(0).getTime(ltime) ||
                MyTime.getIntervalValue(args.get(1), intType, strValue, interval)))
            return true;

        if (dateSubInterval)
            interval.setNeg(!interval.isNeg());

        long usec1 = ((((ltime.getDay() * 24 + ltime.getHour()) * 60 + ltime.getMinute()) * 60 + ltime.getSecond()) * 1000000L +
                ltime.getSecondPart()) * (ltime.isNeg() ? -1 : 1);
        long usec2 = ((((interval.getDay() * 24 + interval.getHour()) * 60 + interval.getMinute()) * 60 + interval.getSecond()) * 1000000L +
                interval.getSecondPart()) * (interval.isNeg() ? -1 : 1);
        long diff = usec1 + usec2;
        LLDivT seconds = new LLDivT();
        seconds.setQuot(diff / 1000000);
        seconds.setRem(diff % 1000000 * 1000); /* time.second_part= lldiv.rem / 1000 */
        if ((nullValue = (interval.getYear() != 0 || interval.getMonth() != 0 || MyTime.secToTime(seconds, ltime)))) {
            LOGGER.info("datetime function overflow!");
            return true;
        }
        return false;
    }

    @Override
    protected boolean valDatetime(MySQLTime ltime, long fuzzyDate) {
        if (cachedFieldType != FieldTypes.MYSQL_TYPE_TIME)
            return getDateInternal(ltime, fuzzyDate | MyTime.TIME_NO_ZERO_DATE);
        return getTimeInternal(ltime);
    }

    @Override
    public SQLExpr toExpression() {
        String funcName = funcName();
        if (dateSubInterval) {
            funcName = "date_sub";
        }
        SQLMethodInvokeExpr method = new SQLMethodInvokeExpr(funcName);
        method.addArgument(args.get(0).toExpression());
        SQLIntervalExpr intervalExpr = new SQLIntervalExpr();
        intervalExpr.setValue(args.get(1).toExpression());
        intervalExpr.setUnit(intType);
        method.addArgument(intervalExpr);
        return method;
    }

    @Override
    protected Item cloneStruct(boolean forCalculate, List<Item> calArgs, boolean isPushDown, List<Field> fields) {
        List<Item> newArgs = null;
        if (!forCalculate)
            newArgs = cloneStructList(args);
        else
            newArgs = calArgs;
        return new ItemDateAddInterval(newArgs.get(0), newArgs.get(1), intType, this.dateSubInterval, charsetIndex);
    }

}
