enum AnimationState
{
    None,
    Auto,
    Zero,
}

export interface Options
{
    duration?: number
    timing?: string
}

export const defaultOptions = {
    duration: 300,
    timing: 'ease',
}

export default class Animator
{
    el: HTMLElement
    state: AnimationState = AnimationState.None
    orgTransition: string = ''

    constructor(el: HTMLElement)
    {
        this.el = el
        this.el.addEventListener('transitionend', this)
    }

    /**
     * Animate the height of the element to 'auto'.
     * 
     * @param options - Animation options
     * @returns The height of the element after the animation completes.
     */
    autoHeight({ duration, timing }: Options = defaultOptions): Promise<number>
    {
        return new Promise(res => {
            const { el } = this
            const style = getComputedStyle(el)

            duration = duration || defaultOptions.duration
    
            this.orgTransition = style.transition
            const transition = [
                this.orgTransition,
                `height ${duration}ms ${timing || defaultOptions.timing}`.trim(),
            ].filter(t => t).join(', ')
    
            this.state = AnimationState.Auto
            el.style.transition = transition
            el.style.height = style.height

            const scrollHeight = `${el.scrollHeight}px`
    
            requestAnimationFrame(() => el.style.height = scrollHeight)
            
            const start = performance.now()

            requestAnimationFrame(function awaitCompletion() {
                const height = getComputedStyle(el).height

                if ((performance.now() - start) < duration! && height !== scrollHeight) {
                    requestAnimationFrame(awaitCompletion)
                    return
                }
                
                res(height ? parseInt(height.replace(/px/, ''), 10) : 0)
            })
        })
    }

    /**
     * Animate the height of the element to '0px'.
     * 
     * @param options - Animation options
     * @param timing - CSS transition timing function
     */
    zeroHeight({ duration, timing }: Options = defaultOptions): Promise<void>
    {
        return new Promise(res => {
            const { el } = this
            const style = getComputedStyle(el)
    
            this.orgTransition = style.transition
            const transition = [
                this.orgTransition,
                `height ${duration || defaultOptions.duration}ms ${timing || defaultOptions.timing}`.trim(),
            ].filter(t => t).join(', ')
    
            this.state = AnimationState.Zero
            el.style.transition = transition
            el.style.height = style.height
    
            requestAnimationFrame(() => el.style.height = '0')

            requestAnimationFrame(function awaitCompletion() {
                let height: string|number|null = getComputedStyle(el).height
                height = height ? parseInt(height.replace(/px/, ''), 10) : 0

                if (height > 0) {
                    requestAnimationFrame(awaitCompletion)
                    return
                }

                res()
            })
        })
    }

    handleEvent(ev: Event)
    {
        switch (ev.type) {
            case 'transitionend':
                this.onTransitionEnded()
                break
        }
    }

    onTransitionEnded()
    {
        if (this.state === AnimationState.Auto) {
            this.el.style.height = 'auto'
        }

        this.el.style.transition = this.orgTransition
        this.orgTransition = ''
        this.state = AnimationState.None
    }
}
