/*
* 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.
*/

import * as zrUtil from 'zrender/src/core/util';
import {
    TimeAxisLabelFormatterDictionary,
    TimeAxisLabelFormatterDictionaryOption,
    TimeAxisLabelFormatterExtraParams,
    TimeAxisLabelFormatterOption,
    TimeAxisLabelFormatterParsed,
    TimeAxisLabelFormatterUpperDictionary,
    TimeAxisLabelLeveledFormatterOption,
} from './../coord/axisCommonTypes';
import * as numberUtil from './number';
import {NullUndefined, ScaleTick} from './types';
import { getDefaultLocaleModel, getLocaleModel, SYSTEM_LANG, LocaleOption } from '../core/locale';
import Model from '../model/Model';
import { getScaleBreakHelper } from '../scale/break';

export const ONE_SECOND = 1000;
export const ONE_MINUTE = ONE_SECOND * 60;
export const ONE_HOUR = ONE_MINUTE * 60;
export const ONE_DAY = ONE_HOUR * 24;
export const ONE_YEAR = ONE_DAY * 365;


const primaryTimeUnitFormatterMatchers: {[key in PrimaryTimeUnit]: RegExp} = {
    year: /({yyyy}|{yy})/,
    month: /({MMMM}|{MMM}|{MM}|{M})/,
    day: /({dd}|{d})/,
    hour: /({HH}|{H}|{hh}|{h})/,
    minute: /({mm}|{m})/,
    second: /({ss}|{s})/,
    millisecond: /({SSS}|{S})/,
} as const;

const defaultFormatterSeed: {[key in PrimaryTimeUnit]: string} = {
    year: '{yyyy}',
    month: '{MMM}',
    day: '{d}',
    hour: '{HH}:{mm}',
    minute: '{HH}:{mm}',
    second: '{HH}:{mm}:{ss}',
    millisecond: '{HH}:{mm}:{ss} {SSS}',
} as const;

const defaultFullFormatter = '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}';
const fullDayFormatter = '{yyyy}-{MM}-{dd}';

export const fullLeveledFormatter = {
    year: '{yyyy}',
    month: '{yyyy}-{MM}',
    day: fullDayFormatter,
    hour: fullDayFormatter + ' ' + defaultFormatterSeed.hour,
    minute: fullDayFormatter + ' ' + defaultFormatterSeed.minute,
    second: fullDayFormatter + ' ' + defaultFormatterSeed.second,
    millisecond: defaultFullFormatter
};

export type JSDateGetterNames =
    'getUTCFullYear' | 'getFullYear'
    | 'getUTCMonth' | 'getMonth'
    | 'getUTCDate' | 'getDate'
    | 'getUTCHours' | 'getHours'
    | 'getUTCMinutes' | 'getMinutes'
    | 'getUTCSeconds' | 'getSeconds'
    | 'getUTCMilliseconds' | 'getMilliseconds'
;
export type JSDateSetterNames =
    'setUTCFullYear' | 'setFullYear'
    | 'setUTCMonth' | 'setMonth'
    | 'setUTCDate' | 'setDate'
    | 'setUTCHours' | 'setHours'
    | 'setUTCMinutes' | 'setMinutes'
    | 'setUTCSeconds' | 'setSeconds'
    | 'setUTCMilliseconds' | 'setMilliseconds'
;

export type PrimaryTimeUnit = (typeof primaryTimeUnits)[number];

export type TimeUnit = (typeof timeUnits)[number];

// Order must be ensured from big to small.
export const primaryTimeUnits = [
    'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond'
] as const;
export const timeUnits = [
    'year', 'half-year', 'quarter', 'month', 'week', 'half-week', 'day',
    'half-day', 'quarter-day', 'hour', 'minute', 'second', 'millisecond'
] as const;


export function parseTimeAxisLabelFormatter(
    formatter: TimeAxisLabelFormatterOption
): TimeAxisLabelFormatterParsed {
    // Keep the logic the same with function `leveledFormat`.
    return (!zrUtil.isString(formatter) && !zrUtil.isFunction(formatter))
        ? parseTimeAxisLabelFormatterDictionary(formatter)
        : formatter;
}

/**
 * The final generated dictionary is like:
 *  generated_dict = {
 *      year: {
 *          year: ['{yyyy}', ...<higher_levels_if_any>]
 *      },
 *      month: {
 *          year: ['{yyyy} {MMM}', ...<higher_levels_if_any>],
 *          month: ['{MMM}', ...<higher_levels_if_any>]
 *      },
 *      day: {
 *          year: ['{yyyy} {MMM} {d}', ...<higher_levels_if_any>],
 *          month: ['{MMM} {d}', ...<higher_levels_if_any>],
 *          day: ['{d}', ...<higher_levels_if_any>]
 *      },
 *      ...
 *  }
 *
 * In echarts option, users can specify the entire dictionary or typically just:
 *  {formatter: {
 *      year: '{yyyy}', // Or an array of leveled templates: `['{yyyy}', '{bold1|{yyyy}}', ...]`,
 *                      // corresponding to `[level0, level1, level2, ...]`.
 *      month: '{MMM}',
 *      day: '{d}',
 *      hour: '{HH}:{mm}',
 *      second: '{HH}:{mm}',
 *      ...
 *  }}
 *  If any time unit is not specified in echarts option, the default template is used,
 *  such as `['{yyyy}', {primary|{yyyy}']`.
 *
 * The `tick.level` is only used to read string from each array, meaning the style type.
 *
 * Let `lowerUnit = getUnitFromValue(tick.value)`.
 * The non-break axis ticks only use `generated_dict[lowerUnit][lowerUnit][level]`.
 * The break axis ticks may use `generated_dict[lowerUnit][upperUnit][level]`, because:
 *  Consider the case: the non-break ticks are `16th, 23th, Feb, 7th, ...`, where `Feb` is in the break
 *  range and pruned by breaks, and the break ends might be in lower time unit than day. e.g., break start
 *  is `Jan 25th 18:00`(in unit `hour`) and break end is `Feb 6th 18:30` (in unit `minute`). Thus the break
 *  label prefers `Jan 25th 18:00` and `Feb 6th 18:30` rather than only `18:00` and `18:30`, otherwise it
 *  causes misleading.
 *  In this case, the tick of the break start and end will both be:
 *      `{level: 1, lowerTimeUnit: 'minute', upperTimeUnit: 'month'}`
 *  And get the final template by `generated_dict[lowerTimeUnit][upperTimeUnit][level]`.
 *  Note that the time unit can not be calculated directly by a single tick value, since the two breaks have
 *  to be at the same time unit to avoid awkward appearance. i.e., `Jan 25th 18:00` is in the time unit "hour"
 *  but we need it to be "minute", following `Feb 6th 18:30`.
 */
function parseTimeAxisLabelFormatterDictionary(
    dictOption: TimeAxisLabelFormatterDictionaryOption | NullUndefined
): TimeAxisLabelFormatterDictionary {
    dictOption = dictOption || {};
    const dict = {} as TimeAxisLabelFormatterDictionary;

    // Currently if any template is specified by user, it may contain rich text tag,
    // such as `'{my_bold|{YYYY}}'`, thus we do add highlight style to it.
    // (Note that nested tag (`'{some|{some2|xxx}}'`) in rich text is not supported yet.)
    let canAddHighlight = true;
    zrUtil.each(primaryTimeUnits, lowestUnit => {
        canAddHighlight &&= dictOption[lowestUnit] == null;
    });

    zrUtil.each(primaryTimeUnits, (lowestUnit, lowestUnitIdx) => {
        const upperDictOption = dictOption[lowestUnit];
        dict[lowestUnit] = {} as TimeAxisLabelFormatterUpperDictionary;

        let lowerTpl: string | null = null;
        for (let upperUnitIdx = lowestUnitIdx; upperUnitIdx >= 0; upperUnitIdx--) {
            const upperUnit = primaryTimeUnits[upperUnitIdx];
            const upperDictItemOption: TimeAxisLabelLeveledFormatterOption =
                (zrUtil.isObject(upperDictOption) && !zrUtil.isArray(upperDictOption))
                    ? upperDictOption[upperUnit]
                    : upperDictOption;

            let tplArr: string[];
            if (zrUtil.isArray(upperDictItemOption)) {
                tplArr = upperDictItemOption.slice();
                lowerTpl = tplArr[0] || '';
            }
            else if (zrUtil.isString(upperDictItemOption)) {
                lowerTpl = upperDictItemOption;
                tplArr = [lowerTpl];
            }
            else {
                if (lowerTpl == null) {
                    lowerTpl = defaultFormatterSeed[lowestUnit];
                }
                // Generate the dict by the rule as follows:
                // If the user specify (or by default):
                //  {formatter: {
                //      year: '{yyyy}',
                //      month: '{MMM}',
                //      day: '{d}',
                //      ...
                //  }}
                // Concat them to make the final dictionary:
                //  {formatter: {
                //      year: {year: ['{yyyy}']},
                //      month: {year: ['{yyyy} {MMM}'], month: ['{MMM}']},
                //      day: {year: ['{yyyy} {MMM} {d}'], month: ['{MMM} {d}'], day: ['{d}']}
                //      ...
                //  }}
                // And then add `{primary|...}` to each array if from default template.
                // This strategy is convinient for user configurating and works for most cases.
                // If bad cases encountered, users can specify the entire dictionary themselves
                // instead of going through this logic.
                else if (!primaryTimeUnitFormatterMatchers[upperUnit].test(lowerTpl)) {
                    lowerTpl = `${dict[upperUnit][upperUnit][0]} ${lowerTpl}`;
                }
                tplArr = [lowerTpl];
                if (canAddHighlight) {
                    tplArr[1] = `{primary|${lowerTpl}}`;
                }
            }
            dict[lowestUnit][upperUnit] = tplArr;
        }
    });
    return dict;
}

export function pad(str: string | number, len: number): string {
    str += '';
    return '0000'.substr(0, len - (str as string).length) + str;
}

export function getPrimaryTimeUnit(timeUnit: TimeUnit): PrimaryTimeUnit {
    switch (timeUnit) {
        case 'half-year':
        case 'quarter':
            return 'month';
        case 'week':
        case 'half-week':
            return 'day';
        case 'half-day':
        case 'quarter-day':
            return 'hour';
        default:
            // year, minutes, second, milliseconds
            return timeUnit;
    }
}

export function isPrimaryTimeUnit(timeUnit: TimeUnit): boolean {
    return timeUnit === getPrimaryTimeUnit(timeUnit);
}

export function getDefaultFormatPrecisionOfInterval(timeUnit: PrimaryTimeUnit): PrimaryTimeUnit {
    switch (timeUnit) {
        case 'year':
        case 'month':
            return 'day';
        case 'millisecond':
            return 'millisecond';
        default:
            // Also for day, hour, minute, second
            return 'second';
    }
}

export function format(
    // Note: The result based on `isUTC` are totally different, which can not be just simply
    // substituted by the result without `isUTC`. So we make the param `isUTC` mandatory.
    time: unknown, template: string, isUTC: boolean, lang?: string | Model<LocaleOption>
): string {
    const date = numberUtil.parseDate(time);
    const y = date[fullYearGetterName(isUTC)]();
    const M = date[monthGetterName(isUTC)]() + 1;
    const q = Math.floor((M - 1) / 3) + 1;
    const d = date[dateGetterName(isUTC)]();
    const e = date['get' + (isUTC ? 'UTC' : '') + 'Day' as 'getDay' | 'getUTCDay']();
    const H = date[hoursGetterName(isUTC)]();
    const h = (H - 1) % 12 + 1;
    const m = date[minutesGetterName(isUTC)]();
    const s = date[secondsGetterName(isUTC)]();
    const S = date[millisecondsGetterName(isUTC)]();
    const a = H >= 12 ? 'pm' : 'am';
    const A = a.toUpperCase();

    const localeModel = lang instanceof Model ? lang
        : getLocaleModel(lang || SYSTEM_LANG) || getDefaultLocaleModel();
    const timeModel = localeModel.getModel('time');
    const month = timeModel.get('month');
    const monthAbbr = timeModel.get('monthAbbr');
    const dayOfWeek = timeModel.get('dayOfWeek');
    const dayOfWeekAbbr = timeModel.get('dayOfWeekAbbr');

    return (template || '')
        .replace(/{a}/g, a + '')
        .replace(/{A}/g, A + '')
        .replace(/{yyyy}/g, y + '')
        .replace(/{yy}/g, pad(y % 100 + '', 2))
        .replace(/{Q}/g, q + '')
        .replace(/{MMMM}/g, month[M - 1])
        .replace(/{MMM}/g, monthAbbr[M - 1])
        .replace(/{MM}/g, pad(M, 2))
        .replace(/{M}/g, M + '')
        .replace(/{dd}/g, pad(d, 2))
        .replace(/{d}/g, d + '')
        .replace(/{eeee}/g, dayOfWeek[e])
        .replace(/{ee}/g, dayOfWeekAbbr[e])
        .replace(/{e}/g, e + '')
        .replace(/{HH}/g, pad(H, 2))
        .replace(/{H}/g, H + '')
        .replace(/{hh}/g, pad(h + '', 2))
        .replace(/{h}/g, h + '')
        .replace(/{mm}/g, pad(m, 2))
        .replace(/{m}/g, m + '')
        .replace(/{ss}/g, pad(s, 2))
        .replace(/{s}/g, s + '')
        .replace(/{SSS}/g, pad(S, 3))
        .replace(/{S}/g, S + '');
}

export function leveledFormat(
    tick: ScaleTick,
    idx: number,
    formatter: TimeAxisLabelFormatterParsed,
    lang: string | Model<LocaleOption>,
    isUTC: boolean
) {
    let template = null;
    if (zrUtil.isString(formatter)) {
        // Single formatter for all units at all levels
        template = formatter;
    }
    else if (zrUtil.isFunction(formatter)) {
        const extra: TimeAxisLabelFormatterExtraParams = {
            time: tick.time,
            level: tick.time ? tick.time.level : 0,
        };
        const scaleBreakHelper = getScaleBreakHelper();
        if (scaleBreakHelper) {
            scaleBreakHelper.makeAxisLabelFormatterParamBreak(extra, tick.break);
        }
        template = formatter(tick.value, idx, extra);
    }
    else {
        const tickTime = tick.time;
        if (tickTime) {
            const leveledTplArr = formatter[tickTime.lowerTimeUnit][tickTime.upperTimeUnit];
            template = leveledTplArr[Math.min(tickTime.level, leveledTplArr.length - 1)] || '';
        }
        else {
            // tick may be from customTicks or timeline therefore no tick.time.
            const unit = getUnitFromValue(tick.value, isUTC);
            template = formatter[unit][unit][0];
        }
    }

    return format(new Date(tick.value), template, isUTC, lang);
}

export function getUnitFromValue(
    value: number | string | Date,
    isUTC: boolean
): PrimaryTimeUnit {
    const date = numberUtil.parseDate(value);
    const M = (date as any)[monthGetterName(isUTC)]() + 1;
    const d = (date as any)[dateGetterName(isUTC)]();
    const h = (date as any)[hoursGetterName(isUTC)]();
    const m = (date as any)[minutesGetterName(isUTC)]();
    const s = (date as any)[secondsGetterName(isUTC)]();
    const S = (date as any)[millisecondsGetterName(isUTC)]();

    const isSecond = S === 0;
    const isMinute = isSecond && s === 0;
    const isHour = isMinute && m === 0;
    const isDay = isHour && h === 0;
    const isMonth = isDay && d === 1;
    const isYear = isMonth && M === 1;

    if (isYear) {
        return 'year';
    }
    else if (isMonth) {
        return 'month';
    }
    else if (isDay) {
        return 'day';
    }
    else if (isHour) {
        return 'hour';
    }
    else if (isMinute) {
        return 'minute';
    }
    else if (isSecond) {
        return 'second';
    }
    else {
        return 'millisecond';
    }
}

// export function getUnitValue(
//     value: number | Date,
//     unit: TimeUnit,
//     isUTC: boolean
// ) : number {
//     const date = zrUtil.isNumber(value)
//         ? numberUtil.parseDate(value)
//         : value;
//     unit = unit || getUnitFromValue(value, isUTC);

//     switch (unit) {
//         case 'year':
//             return date[fullYearGetterName(isUTC)]();
//         case 'half-year':
//             return date[monthGetterName(isUTC)]() >= 6 ? 1 : 0;
//         case 'quarter':
//             return Math.floor((date[monthGetterName(isUTC)]() + 1) / 4);
//         case 'month':
//             return date[monthGetterName(isUTC)]();
//         case 'day':
//             return date[dateGetterName(isUTC)]();
//         case 'half-day':
//             return date[hoursGetterName(isUTC)]() / 24;
//         case 'hour':
//             return date[hoursGetterName(isUTC)]();
//         case 'minute':
//             return date[minutesGetterName(isUTC)]();
//         case 'second':
//             return date[secondsGetterName(isUTC)]();
//         case 'millisecond':
//             return date[millisecondsGetterName(isUTC)]();
//     }
// }

/**
 * e.g.,
 * If timeUnit is 'year', return the Jan 1st 00:00:00 000 of that year.
 * If timeUnit is 'day', return the 00:00:00 000 of that day.
 *
 * @return The input date.
 */
export function roundTime(date: Date, timeUnit: PrimaryTimeUnit, isUTC: boolean): Date {
    switch (timeUnit) {
        case 'year':
            date[monthSetterName(isUTC)](0);
        case 'month':
            date[dateSetterName(isUTC)](1);
        case 'day':
            date[hoursSetterName(isUTC)](0);
        case 'hour':
            date[minutesSetterName(isUTC)](0);
        case 'minute':
            date[secondsSetterName(isUTC)](0);
        case 'second':
            date[millisecondsSetterName(isUTC)](0);
    }
    return date;
}

export function fullYearGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCFullYear' : 'getFullYear';
}

export function monthGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCMonth' : 'getMonth';
}

export function dateGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCDate' : 'getDate';
}

export function hoursGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCHours' : 'getHours';
}

export function minutesGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCMinutes' : 'getMinutes';
}

export function secondsGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCSeconds' : 'getSeconds';
}

export function millisecondsGetterName(isUTC: boolean) {
    return isUTC ? 'getUTCMilliseconds' : 'getMilliseconds';
}

export function fullYearSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCFullYear' : 'setFullYear';
}

export function monthSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCMonth' : 'setMonth';
}

export function dateSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCDate' : 'setDate';
}

export function hoursSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCHours' : 'setHours';
}

export function minutesSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCMinutes' : 'setMinutes';
}

export function secondsSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCSeconds' : 'setSeconds';
}

export function millisecondsSetterName(isUTC: boolean) {
    return isUTC ? 'setUTCMilliseconds' : 'setMilliseconds';
}
