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


const ORIGIN_METHOD = '\0__throttleOriginMethod' as const;
const RATE = '\0__throttleRate' as const;
const THROTTLE_TYPE = '\0__throttleType' as const;

type ThrottleFunction = (this: unknown, ...args: unknown[]) => void;
export type ThrottleType = 'fixRate' | 'debounce';

export interface ThrottleController {
    clear(): void;
    debounceNextCall(debounceDelay: number): void;
};

/**
 * @public
 * @param {(Function)} fn
 * @param {number} [delay=0] Unit: ms.
 * @param {boolean} [debounce=false]
 *        true: If call interval less than `delay`, only the last call works.
 *        false: If call interval less than `delay, call works on fixed rate.
 * @return {(Function)} throttled fn.
 */
export function throttle<T extends ThrottleFunction>(
    fn: T,
    delay?: number,
    debounce?: boolean
): T & ThrottleController {

    let currCall;
    let lastCall = 0;
    let lastExec = 0;
    let timer: ReturnType<typeof setTimeout> = null;
    let diff;
    let scope: unknown;
    let args: unknown[];
    let debounceNextCall: number;

    delay = delay || 0;

    function exec(): void {
        lastExec = (new Date()).getTime();
        timer = null;
        fn.apply(scope, args || []);
    }

    const cb = function (this: unknown, ...cbArgs: unknown[]): void {
        currCall = (new Date()).getTime();
        scope = this;
        args = cbArgs;
        const thisDelay = debounceNextCall || delay;
        const thisDebounce = debounceNextCall || debounce;
        debounceNextCall = null;
        diff = currCall - (thisDebounce ? lastCall : lastExec) - thisDelay;

        clearTimeout(timer);

        // Here we should make sure that: the `exec` SHOULD NOT be called later
        // than a new call of `cb`, that is, preserving the command order. Consider
        // calculating "scale rate" when roaming as an example. When a call of `cb`
        // happens, either the `exec` is called dierectly, or the call is delayed.
        // But the delayed call should never be later than next call of `cb`. Under
        // this assurance, we can simply update view state each time `dispatchAction`
        // triggered by user roaming, but not need to add extra code to avoid the
        // state being "rolled-back".
        if (thisDebounce) {
            timer = setTimeout(exec, thisDelay);
        }
        else {
            if (diff >= 0) {
                exec();
            }
            else {
                timer = setTimeout(exec, -diff);
            }
        }

        lastCall = currCall;
    } as T & ThrottleController;

    /**
     * Clear throttle.
     * @public
     */
    cb.clear = function (): void {
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
    };

    /**
     * Enable debounce once.
     */
    cb.debounceNextCall = function (debounceDelay: number): void {
        debounceNextCall = debounceDelay;
    };

    return cb;
}

/**
 * Create throttle method or update throttle rate.
 *
 * @example
 * ComponentView.prototype.render = function () {
 *     ...
 *     throttle.createOrUpdate(
 *         this,
 *         '_dispatchAction',
 *         this.model.get('throttle'),
 *         'fixRate'
 *     );
 * };
 * ComponentView.prototype.remove = function () {
 *     throttle.clear(this, '_dispatchAction');
 * };
 * ComponentView.prototype.dispose = function () {
 *     throttle.clear(this, '_dispatchAction');
 * };
 *
 */
export function createOrUpdate<T, S extends keyof T, P = T[S]>(
    obj: T,
    fnAttr: S,
    rate: number,
    throttleType: ThrottleType
): P extends ThrottleFunction ? P & ThrottleController : never {
    let fn = obj[fnAttr];

    if (!fn) {
        return;
    }

    const originFn = (fn as any)[ORIGIN_METHOD] || fn;
    const lastThrottleType = (fn as any)[THROTTLE_TYPE];
    const lastRate = (fn as any)[RATE];

    if (lastRate !== rate || lastThrottleType !== throttleType) {
        if (rate == null || !throttleType) {
            return (obj[fnAttr] = originFn);
        }

        fn = obj[fnAttr] = throttle(
            originFn, rate, throttleType === 'debounce'
        );
        (fn as any)[ORIGIN_METHOD] = originFn;
        (fn as any)[THROTTLE_TYPE] = throttleType;
        (fn as any)[RATE] = rate;
    }

    return fn as ReturnType<typeof createOrUpdate>;
}

/**
 * Clear throttle. Example see throttle.createOrUpdate.
 */
export function clear<T, S extends keyof T>(obj: T, fnAttr: S): void {
    const fn = obj[fnAttr];
    if (fn && (fn as any)[ORIGIN_METHOD]) {
        obj[fnAttr] = (fn as any)[ORIGIN_METHOD];
    }
}
