import { Host, h, writeTask } from "@stencil/core";
import { getIonMode, } from '../../global/ionic-global';
import { getTimeGivenProgression, Point } from '../../utils/animation/cubic-bezier';
import { clamp, getElementRoot, raf, transitionEndAsync } from '../../utils/helpers';
import { createPullingAnimation, createSnapBackAnimation, getRefresherAnimationType, } from './refresher.utils';
export class Refresher {
    constructor() {
        // @ts-ignore
        this.appliedStyles = false;
        // @ts-ignore
        this.didStart = false;
        this.progress = 0;
        this.nativeRefresher = false;
        this.animations = [];
        this.pointerDown = false;
        // @ts-ignore
        this.needsCompletion = false;
        // @ts-ignore
        this.didRefresh = false;
        /**
         * The current state which the refresher is in. The refresher's states include:
         *
         * - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden.
         * - `pulling` - The user is actively pulling down the refresher, but has not reached the point yet that if the user lets go, it'll refresh.
         * - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed.
         * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state.
         * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state.
         * - `completing` - The `refreshing` state has finished and the refresher is in the way of closing itself. Once closed, the refresher will go back to the `inactive` state.
         */
        this.state = 1 /* Inactive */;
        /**
         * The minimum distance the user must pull down until the
         * refresher will go into the `refreshing` state.
         */
        this.pullMin = 60;
        /**
         * The maximum distance of the pull until the refresher
         * will automatically go into the `refreshing` state.
         * Defaults to the result of `pullMin + 60`.
         */
        this.pullMax = this.pullMin + 60;
        /**
         * Time it takes to close the refresher.
         */
        this.closeDuration = '280ms';
        /**
         * Time it takes the refresher to to snap back to the `refreshing` state.
         */
        this.snapbackDuration = '280ms';
        /**
         * How much to multiply the pull speed by. To slow the pull animation down,
         * pass a number less than `1`. To speed up the pull, pass a number greater
         * than `1`. The default value is `1` which is equal to the speed of the cursor.
         * If a negative value is passed in, the factor will be `1` instead.
         *
         * For example: If the value passed is `1.2` and the content is dragged by
         * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
         * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
         * will be `8` pixels, less than the amount the cursor has moved.
         */
        this.pullFactor = 1;
        /**
         * If `true`, the refresher will be hidden.
         */
        this.disabled = false;
    }
    disabledChanged() {
        if (this.gesture) {
            this.gesture.setDisabled(this.disabled);
        }
    }
    async setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner) {
        const circle = getElementRoot(pullingSpinner).querySelector('circle');
        const pullingRefresherIcon = this.el.querySelector('ion-refresher-content .refresher-pulling-icon');
        const refreshingCircle = getElementRoot(refreshingSpinner).querySelector('circle');
        if (circle !== null && refreshingCircle !== null) {
            writeTask(() => {
                circle.style.setProperty('animation', 'none');
                // This lines up the animation on the refreshing spinner with the pulling spinner
                refreshingSpinner.style.setProperty('animation-delay', '-655ms');
                refreshingCircle.style.setProperty('animation-delay', '-655ms');
            });
        }
        this.gesture = (await import('../../utils/gesture')).createGesture({
            el: this.scrollEl,
            gestureName: 'refresher',
            gesturePriority: 31,
            direction: 'y',
            threshold: 5,
            canStart: () => this.state !== 8 /* Refreshing */ && this.state !== 32 /* Completing */ && this.scrollEl.scrollTop === 0,
            onStart: (ev) => {
                ev.data = { animation: undefined, didStart: false, cancelled: false };
            },
            onMove: (ev) => {
                if ((ev.velocityY < 0 && this.progress === 0 && !ev.data.didStart) || ev.data.cancelled) {
                    ev.data.cancelled = true;
                    return;
                }
                if (!ev.data.didStart) {
                    ev.data.didStart = true;
                    this.state = 2 /* Pulling */;
                    writeTask(() => this.scrollEl.style.setProperty('--overflow', 'hidden'));
                    const animationType = getRefresherAnimationType(contentEl);
                    const animation = createPullingAnimation(animationType, pullingRefresherIcon, this.el);
                    ev.data.animation = animation;
                    animation.progressStart(false);
                    this.ionStart.emit();
                    this.animations.push(animation);
                    return;
                }
                // Since we are using an easing curve, slow the gesture tracking down a bit
                this.progress = clamp(0, (ev.deltaY / 180) * 0.5, 1);
                ev.data.animation.progressStep(this.progress);
                this.ionPull.emit();
            },
            onEnd: (ev) => {
                if (!ev.data.didStart) {
                    return;
                }
                writeTask(() => this.scrollEl.style.removeProperty('--overflow'));
                if (this.progress <= 0.4) {
                    // this.gesture!.enable(false);
                    this.gesture.setDisabled(true);
                    ev.data.animation
                        .progressEnd(0, this.progress, 500)
                        .onFinish(() => {
                        this.animations.forEach(ani => ani.destroy());
                        this.animations = [];
                        // this.gesture!.enable(true);
                        this.gesture.setDisabled(false);
                        this.state = 1 /* Inactive */;
                    });
                    return;
                }
                const progress = getTimeGivenProgression(new Point(0, 0), new Point(0, 0), new Point(1, 1), new Point(1, 1), this.progress);
                const snapBackAnimation = createSnapBackAnimation(pullingRefresherIcon);
                this.animations.push(snapBackAnimation);
                writeTask(async () => {
                    pullingRefresherIcon.style.setProperty('--ion-pulling-refresher-translate', `${(progress * 100)}px`);
                    ev.data.animation.progressEnd(1, 1);
                    await snapBackAnimation.play();
                    this.beginRefresh();
                    ev.data.animation.destroy();
                });
            }
        });
        this.disabledChanged();
    }
    async setupNativeRefresher(contentEl) {
        if (this.scrollListenerCallback || !contentEl || this.nativeRefresher || !this.scrollEl) {
            return;
        }
        /**
         * If using non-native refresher before make sure
         * we clean up any old CSS. This can happen when
         * a user manually calls the refresh method in a
         * component create callback before the native
         * refresher is setup.
         */
        this.setCss(0, '', false, '');
        this.nativeRefresher = true;
        const pullingSpinner = this.el.querySelector('ion-refresher-content .refresher-pulling ion-spinner');
        const refreshingSpinner = this.el.querySelector('ion-refresher-content .refresher-refreshing ion-spinner');
        this.setupMDNativeRefresher(contentEl, pullingSpinner, refreshingSpinner);
    }
    async connectedCallback() {
        if (this.el.getAttribute('slot') !== 'fixed') {
            console.error('Make sure you use: <ion-refresher slot="fixed">');
            return;
        }
        const contentEl = this.el.closest('ion-content');
        if (!contentEl) {
            console.error('<ion-refresher> must be used inside an <ion-content>');
            return;
        }
        this.scrollEl = await contentEl.getScrollElement();
        this.backgroundContentEl = getElementRoot(contentEl).querySelector('#background-content');
        this.setupNativeRefresher(contentEl);
        this.initialized.emit();
        // this.disabledChanged();
    }
    disconnectedCallback() {
        this.destroyNativeRefresher();
        this.scrollEl = undefined;
        if (this.gesture) {
            this.gesture.destroy();
            this.gesture = undefined;
        }
    }
    componentDidUpdate() {
        this.checkNativeRefresher();
    }
    async checkNativeRefresher() {
        const useNativeRefresher = true;
        if (useNativeRefresher && !this.nativeRefresher) {
            const contentEl = this.el.closest('ion-content');
            this.setupNativeRefresher(contentEl);
        }
        else if (!useNativeRefresher) {
            this.destroyNativeRefresher();
        }
    }
    destroyNativeRefresher() {
        if (this.scrollEl && this.scrollListenerCallback) {
            this.scrollEl.removeEventListener('scroll', this.scrollListenerCallback);
            this.scrollListenerCallback = undefined;
        }
        this.nativeRefresher = false;
    }
    async resetNativeRefresher(__el, state) {
        this.state = state;
        await transitionEndAsync(this.el.querySelector('.refresher-refreshing-icon'), 200);
        this.didRefresh = false;
        this.needsCompletion = false;
        this.pointerDown = false;
        this.animations.forEach(ani => ani.destroy());
        this.animations = [];
        this.progress = 0;
        this.state = 1 /* Inactive */;
    }
    /**
     * Call `complete()` when your async operation has completed.
     * For example, the `refreshing` state is while the app is performing
     * an asynchronous operation, such as receiving more data from an
     * AJAX request. Once the data has been received, you then call this
     * method to signify that the refreshing has completed and to close
     * the refresher. This method also changes the refresher's state from
     * `refreshing` to `completing`.
     */
    async complete() {
        if (this.nativeRefresher) {
            this.needsCompletion = true;
            // Do not reset scroll el until user removes pointer from screen
            if (!this.pointerDown) {
                raf(() => raf(() => this.resetNativeRefresher(undefined, 32 /* Completing */)));
            }
        }
        else {
            this.close(32 /* Completing */, '120ms');
        }
    }
    /**
     * Changes the refresher's state from `refreshing` to `cancelling`.
     */
    async cancel() {
        if (this.nativeRefresher) {
            // Do not reset scroll el until user removes pointer from screen
            if (!this.pointerDown) {
                raf(() => raf(() => this.resetNativeRefresher(undefined, 16 /* Cancelling */)));
            }
        }
        else {
            this.close(16 /* Cancelling */, '');
        }
    }
    /**
     * A number representing how far down the user has pulled.
     * The number `0` represents the user hasn't pulled down at all. The
     * number `1`, and anything greater than `1`, represents that the user
     * has pulled far enough down that when they let go then the refresh will
     * happen. If they let go and the number is less than `1`, then the
     * refresh will not happen, and the content will return to it's original
     * position.
     */
    getProgress() {
        return Promise.resolve(this.progress);
    }
    /**
     * programmatically trigger refresh
     *
     */
    async autoRefresh() {
        if (!this.canStart()) {
            return false;
        }
        if ((this.state & 56 /* _BUSY_ */) !== 0) {
            return false;
        }
        this.progress = 100;
        this.didStart = true;
        this.ionStart.emit();
        this.beginRefresh();
        return true;
    }
    canStart() {
        if (!this.scrollEl) {
            return false;
        }
        if (this.state !== 1 /* Inactive */) {
            return false;
        }
        // if the scrollTop is greater than zero then it's
        // not possible to pull the content down yet
        if (this.scrollEl.scrollTop > 0) {
            return false;
        }
        return true;
    }
    beginRefresh() {
        // assumes we're already back in a zone
        // they pulled down far enough, so it's ready to refresh
        this.state = 8 /* Refreshing */;
        // place the content in a hangout position while it thinks
        this.setCss(this.pullMin, this.snapbackDuration, true, '');
        // emit "refresh" because it was pulled down far enough
        // and they let go to begin refreshing
        this.ionRefresh.emit({
            complete: this.complete.bind(this)
        });
    }
    close(state, delay) {
        // create fallback timer incase something goes wrong with transitionEnd event
        setTimeout(() => {
            this.state = 1 /* Inactive */;
            this.progress = 0;
            this.didStart = false;
            this.setCss(0, '0ms', false, '');
        }, 600);
        // reset set the styles on the scroll element
        // set that the refresh is actively cancelling/completing
        this.state = state;
        this.setCss(0, this.closeDuration, true, delay);
        // TODO: stop gesture
    }
    setCss(y, duration, overflowVisible, delay) {
        if (this.nativeRefresher) {
            return;
        }
        this.appliedStyles = (y > 0);
        writeTask(() => {
            if (this.scrollEl && this.backgroundContentEl) {
                const scrollStyle = this.scrollEl.style;
                const backgroundStyle = this.backgroundContentEl.style;
                scrollStyle.transform = backgroundStyle.transform = ((y > 0) ? `translateY(${y}px) translateZ(0px)` : '');
                scrollStyle.transitionDuration = backgroundStyle.transitionDuration = duration;
                scrollStyle.transitionDelay = backgroundStyle.transitionDelay = delay;
                scrollStyle.overflow = (overflowVisible ? 'hidden' : '');
            }
        });
    }
    render() {
        const mode = getIonMode(this);
        return (h(Host, { slot: "fixed", class: {
                [mode]: true,
                // Used internally for styling
                [`refresher-${mode}`]: true,
                'refresher-native': this.nativeRefresher,
                'refresher-active': this.state !== 1 /* Inactive */,
                'refresher-pulling': this.state === 2 /* Pulling */,
                'refresher-ready': this.state === 4 /* Ready */,
                'refresher-refreshing': this.state === 8 /* Refreshing */,
                'refresher-cancelling': this.state === 16 /* Cancelling */,
                'refresher-completing': this.state === 32 /* Completing */
            } }));
    }
    static get is() { return "ion-refresher"; }
    static get originalStyleUrls() { return {
        "ios": ["refresher.ios.scss"],
        "md": ["refresher.md.scss"]
    }; }
    static get styleUrls() { return {
        "ios": ["refresher.ios.css"],
        "md": ["refresher.md.css"]
    }; }
    static get properties() { return {
        "pullMin": {
            "type": "number",
            "mutable": false,
            "complexType": {
                "original": "number",
                "resolved": "number",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "The minimum distance the user must pull down until the\nrefresher will go into the `refreshing` state."
            },
            "attribute": "pull-min",
            "reflect": false,
            "defaultValue": "60"
        },
        "pullMax": {
            "type": "number",
            "mutable": false,
            "complexType": {
                "original": "number",
                "resolved": "number",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "The maximum distance of the pull until the refresher\nwill automatically go into the `refreshing` state.\nDefaults to the result of `pullMin + 60`."
            },
            "attribute": "pull-max",
            "reflect": false,
            "defaultValue": "this.pullMin + 60"
        },
        "closeDuration": {
            "type": "string",
            "mutable": false,
            "complexType": {
                "original": "string",
                "resolved": "string",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "Time it takes to close the refresher."
            },
            "attribute": "close-duration",
            "reflect": false,
            "defaultValue": "'280ms'"
        },
        "snapbackDuration": {
            "type": "string",
            "mutable": false,
            "complexType": {
                "original": "string",
                "resolved": "string",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "Time it takes the refresher to to snap back to the `refreshing` state."
            },
            "attribute": "snapback-duration",
            "reflect": false,
            "defaultValue": "'280ms'"
        },
        "pullFactor": {
            "type": "number",
            "mutable": false,
            "complexType": {
                "original": "number",
                "resolved": "number",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "How much to multiply the pull speed by. To slow the pull animation down,\npass a number less than `1`. To speed up the pull, pass a number greater\nthan `1`. The default value is `1` which is equal to the speed of the cursor.\nIf a negative value is passed in, the factor will be `1` instead.\n\nFor example: If the value passed is `1.2` and the content is dragged by\n`10` pixels, instead of `10` pixels the content will be pulled by `12` pixels\n(an increase of 20 percent). If the value passed is `0.8`, the dragged amount\nwill be `8` pixels, less than the amount the cursor has moved."
            },
            "attribute": "pull-factor",
            "reflect": false,
            "defaultValue": "1"
        },
        "disabled": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": "If `true`, the refresher will be hidden."
            },
            "attribute": "disabled",
            "reflect": false,
            "defaultValue": "false"
        }
    }; }
    static get states() { return {
        "state": {}
    }; }
    static get events() { return [{
            "method": "ionRefresh",
            "name": "ionRefresh",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emitted when the user lets go of the content and has pulled down\nfurther than the `pullMin` or pulls the content down and exceeds the pullMax.\nUpdates the refresher state to `refreshing`. The `complete()` method should be\ncalled when the async operation has completed."
            },
            "complexType": {
                "original": "RefresherEventDetail",
                "resolved": "RefresherEventDetail",
                "references": {
                    "RefresherEventDetail": {
                        "location": "import",
                        "path": "../../interface"
                    }
                }
            }
        }, {
            "method": "ionPull",
            "name": "ionPull",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emitted while the user is pulling down the content and exposing the refresher."
            },
            "complexType": {
                "original": "void",
                "resolved": "void",
                "references": {}
            }
        }, {
            "method": "ionStart",
            "name": "ionStart",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emitted when the user begins to start pulling down."
            },
            "complexType": {
                "original": "void",
                "resolved": "void",
                "references": {}
            }
        }, {
            "method": "initialized",
            "name": "initialized",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": "Emitted when component initialized."
            },
            "complexType": {
                "original": "void",
                "resolved": "void",
                "references": {}
            }
        }]; }
    static get methods() { return {
        "complete": {
            "complexType": {
                "signature": "() => Promise<void>",
                "parameters": [],
                "references": {
                    "Promise": {
                        "location": "global"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "Call `complete()` when your async operation has completed.\nFor example, the `refreshing` state is while the app is performing\nan asynchronous operation, such as receiving more data from an\nAJAX request. Once the data has been received, you then call this\nmethod to signify that the refreshing has completed and to close\nthe refresher. This method also changes the refresher's state from\n`refreshing` to `completing`.",
                "tags": []
            }
        },
        "cancel": {
            "complexType": {
                "signature": "() => Promise<void>",
                "parameters": [],
                "references": {
                    "Promise": {
                        "location": "global"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "Changes the refresher's state from `refreshing` to `cancelling`.",
                "tags": []
            }
        },
        "getProgress": {
            "complexType": {
                "signature": "() => Promise<number>",
                "parameters": [],
                "references": {
                    "Promise": {
                        "location": "global"
                    }
                },
                "return": "Promise<number>"
            },
            "docs": {
                "text": "A number representing how far down the user has pulled.\nThe number `0` represents the user hasn't pulled down at all. The\nnumber `1`, and anything greater than `1`, represents that the user\nhas pulled far enough down that when they let go then the refresh will\nhappen. If they let go and the number is less than `1`, then the\nrefresh will not happen, and the content will return to it's original\nposition.",
                "tags": []
            }
        },
        "autoRefresh": {
            "complexType": {
                "signature": "() => Promise<Boolean>",
                "parameters": [],
                "references": {
                    "Promise": {
                        "location": "global"
                    },
                    "Boolean": {
                        "location": "global"
                    }
                },
                "return": "Promise<Boolean>"
            },
            "docs": {
                "text": "programmatically trigger refresh",
                "tags": []
            }
        }
    }; }
    static get elementRef() { return "el"; }
    static get watchers() { return [{
            "propName": "disabled",
            "methodName": "disabledChanged"
        }]; }
}
