// Copyright (c) 2020 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln


import { Stereotype }         from '../types';
import { DatePattern,
         DateTimePattern,
         DateTimeNoTzPattern } from '../lib/util';



const FyPattern = /^first-date-of-fy\(([0-9]+)\)$/;
const FormulaPattern = /^([-+@])([0-9]+)(yr|mo|day|days|hr|min|sec|ms)$/;


class UtcDate extends Date {
    public constructor();
    // tslint:disable-next-line: unified-signatures
    public constructor(str: string);
    public constructor(
        year: number, month: number, date?: number,
        hours?: number, minutes?: number, seconds?: number, ms?: number)
    public constructor(
        year?: number | string, month?: number, date?: number,
        hours?: number, minutes?: number, seconds?: number, ms?: number) {

        super();
        if (year === void 0) {
            return;
        }
        if (typeof year === 'string') {
            if (DateTimePattern.test(year)) {
                // string parameter is expected to be treated as specified TZ
                this.setTime(Date.parse(year)); // returns date in specified TZ
            } else if (DatePattern.test(year)) {
                // string parameter is expected to be treated as UTC
                const d = new Date(year);       // returns date in UTC TZ (getUTC??? returns string parameter's date & time digits)
                this.setTime(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
            } else if (DateTimeNoTzPattern.test(year)) {
                // string parameter is expected to be treated as UTC
                const d = new Date(year);       // returns date in local TZ (get??? returns string parameter's date & time digits)
                this.setTime(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(),
                    d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()));
            } else {
                this.setTime(NaN);
            }
            return;
        }

        this.setUTCDate(1);

        this.setUTCFullYear(year);
        this.setUTCMonth(typeof month === 'number' ? month : 0);
        this.setUTCDate(typeof date === 'number' ? date : 1);
        this.setUTCHours(typeof hours === 'number' ? hours : 0);
        this.setUTCMinutes(typeof minutes === 'number' ? minutes : 0);
        this.setUTCSeconds(typeof seconds === 'number' ? seconds : 0);
        this.setUTCMilliseconds(typeof ms === 'number' ? ms : 0);
    }

    public getFullYear(): number {
        return this.getUTCFullYear();
    }

    public getMonth(): number {
        return this.getUTCMonth();
    }

    public getDate(): number {
        return this.getUTCDate();
    }

    public getHours(): number {
        return this.getUTCHours();
    }

    public getMinutes(): number {
        return this.getUTCMinutes();
    }

    public getSeconds(): number {
        return this.getUTCSeconds();
    }

    public getMilliseconds(): number {
        return this.getUTCMilliseconds();
    }

    // NOTE: set???() are not overridden!
}


class LcDate extends Date {
    public constructor();
    // tslint:disable-next-line: unified-signatures
    public constructor(str: string);
    public constructor(
        year: number, month: number, date?: number,
        hours?: number, minutes?: number, seconds?: number, ms?: number)
    public constructor(
        year?: number | string, month?: number, date?: number,
        hours?: number, minutes?: number, seconds?: number, ms?: number) {

        super();
        if (year === void 0) {
            return;
        }
        if (typeof year === 'string') {
            if (DateTimePattern.test(year)) {
                // string parameter is expected to be treated as specified TZ
                this.setTime(Date.parse(year)); // returns date in specified TZ
            } else if (DatePattern.test(year)) {
                // string parameter is expected to be treated as local TZ
                const d = new Date(year);       // returns date in UTC TZ (getUTC??? returns string parameter's date & time digits)
                const l = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
                this.setTime(l.getTime());
            } else if (DateTimeNoTzPattern.test(year)) {
                // string parameter is expected to be treated as local TZ
                const d = new Date(year);       // returns date in local TZ (get??? returns string parameter's date & time digits)
                this.setTime(d.getTime());
            } else {
                this.setTime(NaN);
            }
            return;
        }

        this.setDate(1);

        this.setFullYear(year);
        this.setMonth(typeof month === 'number' ? month : 0);
        this.setDate(typeof date === 'number' ? date : 1);
        this.setHours(typeof hours === 'number' ? hours : 0);
        this.setMinutes(typeof minutes === 'number' ? minutes : 0);
        this.setSeconds(typeof seconds === 'number' ? seconds : 0);
        this.setMilliseconds(typeof ms === 'number' ? ms : 0);
    }
}


interface DateConstructor {
    new (): Date;
    // tslint:disable-next-line: unified-signatures
    new (str: string): Date;
    new (year: number, month: number, date?: number,
         hours?: number, minutes?: number, seconds?: number, ms?: number): Date;
}


function evaluateFormulaBase(dateCtor: DateConstructor, valueOrFormula: string): Date {
    const errMsg = `evaluateFormula: invalid parameter ${valueOrFormula}`;
    if (typeof valueOrFormula !== 'string') {
        throw new Error(errMsg);
    }
    if (valueOrFormula.startsWith('=')) {
        const formula = valueOrFormula.slice(1).split(' ');
        let d = new dateCtor();
        const now = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes());
        const today = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate());
        d = now;
        for (const f of formula) {
            switch (f) {
            case 'current': case 'now':
                d = now;
                break;
            case 'today':
                d = today;
                break;
            case 'first-date-of-yr': case 'first-date-of-fy(1)':
                d = new dateCtor(d.getFullYear(), 0, 1);
                break;
            case 'last-date-of-yr':
                d = new dateCtor(d.getFullYear(), 11, 31);
                break;
            case 'first-date-of-mo':
                d = new dateCtor(d.getFullYear(), d.getMonth(), 1);
                break;
            case 'last-date-of-mo':
                d = new dateCtor(d.getFullYear(), d.getMonth() + 1, 0);
                break;
            default:
                if (f.startsWith('first-date-of-fy(')) {
                    const m = FyPattern.exec(f);
                    if (m) {
                        const n = Number.parseInt(m[1], 10);
                        if (0 < n && n <= 12) {
                            const mo = d.getMonth() + 1;
                            let yr = d.getFullYear();
                            if (mo < n) {
                                yr--;
                            }
                            d = new dateCtor(yr, n - 1, 1);
                        } else {
                            throw new Error(errMsg);
                        }
                    } else {
                        throw new Error(errMsg);
                    }
                } else {
                    const m = FormulaPattern.exec(f);
                    if (m) {
                        let n = Number.parseInt(m[2], 10);
                        switch (m[3]) {
                        case 'yr':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getFullYear() + n;
                                break;
                            case '-':
                                n = d.getFullYear() - n;
                                break;
                            }
                            d = new dateCtor(n, d.getMonth(), d.getDate(),
                                d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
                            break;
                        case 'mo':
                            switch (m[1]) {
                            case '@':
                                n -= 1;
                                break;
                            case '+':
                                n = d.getMonth() + n;
                                break;
                            case '-':
                                n = d.getMonth() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), n, d.getDate(),
                                d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
                            break;
                        case 'day': case 'days':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getDate() + n;
                                break;
                            case '-':
                                n = d.getDate() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), d.getMonth(), n,
                                d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
                            break;
                        case 'hr':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getHours() + n;
                                break;
                            case '-':
                                n = d.getHours() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate(),
                                n, d.getMinutes(), d.getSeconds(), d.getMilliseconds());
                            break;
                        case 'min':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getMinutes() + n;
                                break;
                            case '-':
                                n = d.getMinutes() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate(),
                                d.getHours(), n, d.getSeconds(), d.getMilliseconds());
                            break;
                        case 'sec':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getSeconds() + n;
                                break;
                            case '-':
                                n = d.getSeconds() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate(),
                                d.getHours(), d.getMinutes(), n, d.getMilliseconds());
                            break;
                        case 'ms':
                            switch (m[1]) {
                            case '@':
                                break;
                            case '+':
                                n = d.getMilliseconds() + n;
                                break;
                            case '-':
                                n = d.getMilliseconds() - n;
                                break;
                            }
                            d = new dateCtor(d.getFullYear(), d.getMonth(), d.getDate(),
                                d.getHours(), d.getMinutes(), d.getSeconds(), n);
                            break;
                        default:
                            throw new Error(errMsg);
                        }
                    } else {
                        if (!(DatePattern.test(f) || DateTimePattern.test(f) || DateTimeNoTzPattern.test(f))) {
                            throw new Error(errMsg);
                        }
                        d = new dateCtor(f);
                    }
                }
            }
        }
        return d;
    } else {
        if (! DatePattern.test(valueOrFormula)) {
            throw new Error(errMsg);
        }
        return new dateCtor(valueOrFormula);
    }
}


export const dateStereotype: Stereotype = {
    tryParse: (value: unknown) => {
        return (
            typeof value === 'string' && DatePattern.test(value)
                ? { value: (new UtcDate(value)).getTime() }
                : null
        );
    },
    evaluateFormula: valueOrFormula => {
        const d = evaluateFormulaBase(UtcDate, valueOrFormula);
        return (new UtcDate(d.getFullYear(), d.getMonth(), d.getDate())).getTime();
    },
    compare: (a: number, b: number) => a - b,
    doCast: false,
};


export const lcDateStereotype: Stereotype = {
    ...dateStereotype,
    tryParse: (value: unknown) => {
        if (typeof value === 'string' && DatePattern.test(value)) {
            return ({ value: (new LcDate(value)).getTime() });
        } else {
            return null;
        }
    },
    evaluateFormula: valueOrFormula => {
        const d = evaluateFormulaBase(LcDate, valueOrFormula);
        return (new LcDate(d.getFullYear(), d.getMonth(), d.getDate())).getTime();
    },
}


export const datetimeStereotype: Stereotype = {
    tryParse: (value: unknown) => {
        return (
            typeof value === 'string' && (DateTimePattern.test(value) || DateTimeNoTzPattern.test(value))
                ? { value: (new UtcDate(value)).getTime() } // If timezone is not specified, it is local time
                : null
        );
    },
    evaluateFormula: valueOrFormula => evaluateFormulaBase(UtcDate, valueOrFormula).getTime(),
    compare: (a: number, b: number) => a - b,
    doCast: false,
};


export const lcDatetimeStereotype: Stereotype = {
    ...datetimeStereotype,
    tryParse: (value: unknown) => {
        return (
            typeof value === 'string' && (DateTimePattern.test(value) || DateTimeNoTzPattern.test(value))
                ? { value: (new LcDate(value)).getTime() }
                : null
        );
    },
    evaluateFormula: valueOrFormula => evaluateFormulaBase(LcDate, valueOrFormula).getTime(),
}


export const stereotypes: Array<[string, Stereotype]> = [
    ['date', dateStereotype],
    ['lcdate', lcDateStereotype],
    ['datetime', datetimeStereotype],
    ['lcdatetime', lcDatetimeStereotype],
];
