import ReactFiberReconciler from 'react-reconciler';
import { useState, useEffect, useRef, useCallback, useMemo, useReducer, useLayoutEffect } from 'react';

const hasOwnProperty = Object.prototype.hasOwnProperty;

// HighRes but slower then Date.now during invoke
// export const now = () => $objc('NSDate').invoke('date').invoke('timeIntervalSince1970') * 1000

const { now } = Date;

const is = {
    obj: a => a === Object(a),
    str: a => typeof a === 'string',
    num: a => typeof a === 'number',
    und: a => a === void 0,
    arr: a => Array.isArray(a),
    equ(a, b) {
        // Wrong type, doesn't match
        if (typeof a !== typeof b) return false
        // Atomic, just compare a against b
        if (is.str(a) || is.num(a) || is.obj(a)) return a === b
        // Array, shallow compare first to see if it's a match
        if (is.arr(a) && a == b) return true
        // Last resort, go through keys
        let i;
        for (i in a) if (!(i in b)) return false
        for (i in b) if (a[i] !== b[i]) return false
        return is.und(i) ? a === b : true
    }
};

function filterProps(oldProps = {}, newProps) {
    const sameProps = Object.keys(newProps).filter(key => is.equ(newProps[key], oldProps[key]));
    const leftOvers = Object.keys(oldProps).filter(key => newProps[key] === void 0);
    const filteredProps = [...sameProps, 'events', 'children', 'key', 'ref'].reduce((acc, prop) => {
        let { [prop]: _, ...rest } = acc;
        return rest
    }, newProps);
    leftOvers.forEach(key => key !== 'children' && (filteredProps[key] = undefined));
    return filteredProps
}

class View {
    constructor(type, props) {
        const { layout, events, animate } = props;
        this._element = $ui.create({
            type,
            props,
            events
        });
        this._layout = layout;
        this._animate = animate;
    }

    _element = null

    _layout = null

    _animate = null

    get element() {
        return this._element
    }

    applyLayout() {
        if (typeof this._layout === 'function') {
            this.element.layout(this._layout);
        }
    }

    updateLayout() {
        if (typeof this._layout === 'function') {
            this.element.updateLayout(this._layout);
        }
    }

    remakeLayout() {
        if (typeof this._layout === 'function') {
            this.element.remakeLayout(this._layout);
        }
    }

    appendChild(child) {
        this.element.add(child.element);
        child.applyLayout();
    }

    removeChild(child) {
        child.element.remove();
    }

    insertBefore(child, beforeChild) {
        this.element.insertBelow(child.element, beforeChild.element);
        child.applyLayout();
    }

    update(updatePayload) {
        let needsUpdateLayout = false;
        if (hasOwnProperty.call(updatePayload, 'layout')) {
            this._layout = updatePayload.layout;
            needsUpdateLayout = true;
            delete updatePayload.layout;
        }
        if (hasOwnProperty.call(updatePayload, 'animate')) {
            this._animate = updatePayload.animate;
            delete updatePayload.animate;
        }
        const element = this.element;
        if (this._animate) {
            const { duration = 0.4, damping = 0, velocity = 0, options = 0, completion = () => {} } = this._animate;
            $ui.animate({
                duration,
                animation() {
                    Object.keys(updatePayload).forEach(prop => {
                        element[prop] = updatePayload[prop];
                    });
                },
                damping,
                velocity,
                options,
                completion
            });
            this.showOverlay();
            return
        }
        Object.keys(updatePayload).forEach(prop => {
            element[prop] = updatePayload[prop];
        });
        needsUpdateLayout && this.updateLayout();
        this.showOverlay();
    }

    showOverlay() {
        if (!global.__REACT_JSBOX_HIGHLIGHT_UPDATES__) {
            return
        }
        const { cornerRadius, smoothCorners, size } = this.element;
        const overlayView = $ui.create({
            type: 'view',
            props: {
                frame: $rect(0, 0, size.width, size.height),
                alpha: 0.6,
                cornerRadius,
                smoothCorners,
                bgcolor: $color('clear'),
                borderColor: $color('#37afa9'),
                borderWidth: 2,
                userInteractionEnabled: false
            }
        });
        this.element.add(overlayView);
        setTimeout(() => {
            overlayView.remove();
        }, 300);
    }
}

const NO_CONTEXT = true;

const hostConfig = {
    now,

    setTimeout,

    clearTimeout,

    scheduleTimeout: setTimeout,

    cancelTimeout: clearTimeout,

    noTimeout: -1,

    supportsMutation: true,

    supportsPersistence: false,

    supportsHydration: false,

    isPrimaryRenderer: true,

    getPublicInstance({ element }) {
        return element
    },

    getRootHostContext() {
        return NO_CONTEXT
    },

    getChildHostContext() {
        return NO_CONTEXT
    },

    prepareForCommit() {
        // noop
    },

    resetAfterCommit() {
        // noop
    },

    createInstance(type, props, internalInstanceHandle) {
        return new View(type, props)
    },

    appendInitialChild(parentInstance, child) {
        parentInstance.appendChild(child);
        child.applyLayout();
    },

    finalizeInitialChildren(parentInstance, type, props) {
        return false
    },

    prepareUpdate(instance, type, oldProps, newProps) {
        return filterProps(oldProps, newProps)
    },

    shouldSetTextContent() {
        return false
    },

    shouldDeprioritizeSubtree(type, props) {
        return !!props.hidden
    },

    createTextInstance() {
        return null
    },

    appendChild(parentInstance, child) {
        parentInstance.appendChild(child);
    },

    appendChildToContainer(parentInstance, child) {
        const parent = parentInstance.element || parentInstance;
        parent.add(child.element);
        child.applyLayout();
    },

    commitMount(instance, updatePayload, type, oldProps, newProps) {
        // noop
    },

    commitUpdate(instance, updatePayload, type, oldProps, newProps) {
        if (updatePayload) {
            instance.update(updatePayload);
        }
    },

    insertBefore(parentInstance, child, beforeChild) {
        parentInstance.insertBefore(child, beforeChild);
    },

    insertInContainerBefore(parentInstance, child, beforeChild) {
        const parent = parentInstance.element || parentInstance;
        parent.insertBelow(child.element, beforeChild.element);
    },

    removeChild(parentInstance, child) {
        parentInstance.removeChild(child);
    },

    removeChildFromContainer(parentInstance, child) {
        child.element.remove();
    },

    resetTextContent() {
        // noop
    },

    hideInstance(instance) {
        instance.element.hidden = true;
    },

    unhideInstance(instance) {
        instance.element.hidden = false;
    },

    hideTextInstance(instance) {
        // noop
    },

    unhideTextInstance(instance, props) {
        // noop
    },

    getFundamentalComponentInstance(fundamentalInstance) {
        throw new Error('Not yet implemented.')
    },

    mountFundamentalComponent(fundamentalInstance) {
        throw new Error('Not yet implemented.')
    },

    shouldUpdateFundamentalComponent(fundamentalInstance) {
        console.warn('Not yet implemented.');
        return false
    },

    updateFundamentalComponent(fundamentalInstance) {
        throw new Error('Not yet implemented.')
    },

    unmountFundamentalComponent(fundamentalInstance) {
        throw new Error('Not yet implemented.')
    },

    cloneFundamentalInstance(fundamentalInstance) {
        throw new Error('Not yet implemented.')
    },

    clearContainer(container) {
        container?.views?.forEach(view => view?.remove());
    },

    getInstanceFromNode() {
        throw new Error('Not yet implemented.')
    },

    beforeActiveInstanceBlur() {
        // noop
    },

    afterActiveInstanceBlur() {
        // noop
    },

    preparePortalMount() {
        // noop
    },

    prepareScopeUpdate() {},

    getInstanceFromScope() {
        throw new Error('Not yet implemented.')
    }
};

const reconciler = ReactFiberReconciler(hostConfig);

const isConcurrent = true;
const hydrate = false;

const defaultOptions = {
    onInit: () => {},
    onRender: () => {}
};

function render(element, container, options) {
    const rendererOptions = Object.assign({}, defaultOptions, options);
    let fiberRoot = container._reactRootContainer;
    if (!fiberRoot) {
        container.views.forEach(view => view.remove());
        const newFiberRoot = reconciler.createContainer(container, isConcurrent, hydrate);
        // eslint-disable-next-line
        fiberRoot = container._reactRootContainer = newFiberRoot;
    }
    rendererOptions.onInit(reconciler);
    return reconciler.updateContainer(element, fiberRoot, null, rendererOptions.onRender)
}

const useCache = (key, initialValue) => {
    const [state, setState] = useState(() => {
        const cacheValue = $cache.get(key);
        if (cacheValue === undefined) {
            $cache.set(key, initialValue);
            return initialValue
        }
        return cacheValue
    });
    useEffect(() => void $cache.set(key, state));

    return [state, setState]
};

function useTimeoutFn(fn, ms = 0) {
    const ready = useRef(false);
    const timeout = useRef();
    const callback = useRef(fn);

    const isReady = useCallback(() => ready.current, []);

    const set = useCallback(() => {
        ready.current = false;
        timeout.current && clearTimeout(timeout.current);

        timeout.current = setTimeout(() => {
            ready.current = true;
            callback.current();
        }, ms);
    }, [ms]);

    const clear = useCallback(() => {
        ready.current = null;
        timeout.current && clearTimeout(timeout.current);
    }, []);

    // update ref when function changes
    useEffect(() => {
        callback.current = fn;
    }, [fn]);

    // set on mount, clear on unmount
    useEffect(() => {
        set();

        return clear
    }, [ms]);

    return [isReady, clear, set]
}

function useDebounce(fn, ms = 0, deps = []) {
    const [isReady, cancel, reset] = useTimeoutFn(fn, ms);

    useEffect(reset, deps);

    return [isReady, cancel]
}

const useEventHandler = (eventHandlerMap, deps) => useMemo(() => eventHandlerMap, deps);

const useFirstMountState = () => {
    const isFirst = useRef(true);

    if (isFirst.current) {
        isFirst.current = false;

        return true
    }

    return isFirst.current
};

const useLatest = value => {
    const ref = useRef(value);
    ref.current = value;
    return ref
};

function useRendersCount() {
    return ++useRef(0).current
}

const updateReducer = num => (num + 1) % 1000000;

const useUpdate = () => {
    const [, update] = useReducer(updateReducer, 0);
    return update
};

const useUpdateEffect = (effect, deps) => {
    const isFirstMount = useFirstMountState();

    useEffect(() => {
        if (!isFirstMount) {
            return effect()
        }
    }, deps);
};

const useUpdateLayoutEffect = (effect, deps) => {
    const isFirstMount = useFirstMountState();

    useLayoutEffect(() => {
        if (!isFirstMount) {
            return effect()
        }
    }, deps);
};

export { render, useCache, useDebounce, useEventHandler, useFirstMountState, useLatest, useRendersCount, useTimeoutFn, useUpdate, useUpdateEffect, useUpdateLayoutEffect };
