import * as React from "react";
import { View, StyleSheet } from "react-native";
import PortalManager from "./PortalManager";

type Props = {
    children: React.ReactNode;
};

type Operation =
    | { type: "mount"; key: number; children: React.ReactNode }
    | { type: "update"; key: number; children: React.ReactNode }
    | { type: "unmount"; key: number };

export type PortalMethods = {
    mount: (children: React.ReactNode) => number;
    update: (key: number, children: React.ReactNode) => void;
    unmount: (key: number) => void;
};

export const PortalContext = React.createContext<PortalMethods>(null as any);

/**
 * Portal host renders all of its children `Portal` elements.
 * For example, you can wrap a screen in `Portal.Host` to render items above the screen.
 * If you're using the `Provider` component, it already includes `Portal.Host`.
 *
 * ## Usage
 * ```js
 * import * as React from 'react';
 * import { Text } from 'react-native';
 * import Portal from 'react-native-simple-elements/components/Portal';
 *
 * const MyComponent = () => (
 *   <Portal.Host>
 *     <Text>Content of the app</Text>
 *   </Portal.Host>
 * );
 *
 * export default MyComponent;
 * ```
 *
 * Here any `Portal` elements under `<App />` are rendered alongside `<App />` and will appear above `<App />` like a `Modal`.
 */
export default class PortalHost extends React.Component<Props> {
    static displayName = "Portal.Host";

    componentDidMount() {
        const manager = this.manager;
        const queue = this.queue;

        while (queue.length && manager) {
            const action = queue.pop();
            if (action) {
                // eslint-disable-next-line default-case
                switch (action.type) {
                case "mount":
                    manager.mount(action.key, action.children);
                    break;
                case "update":
                    manager.update(action.key, action.children);
                    break;
                case "unmount":
                    manager.unmount(action.key);
                    break;
                }
            }
        }
    }

    private setManager = (manager: PortalManager | undefined | null) => {
        this.manager = manager;
    };

    private mount = (children: React.ReactNode) => {
        const key = this.nextKey++;

        if (this.manager) {
            this.manager.mount(key, children);
        } else {
            this.queue.push({ type: "mount", key, children });
        }

        return key;
    };

    private update = (key: number, children: React.ReactNode) => {
        if (this.manager) {
            this.manager.update(key, children);
        } else {
            const op = { type: "mount", key, children };
            const index = this.queue.findIndex(
                (o) => o.type === "mount" || (o.type === "update" && o.key === key)
            );

            if (index > -1) {
                // @ts-ignore
                this.queue[index] = op;
            } else {
                this.queue.push(op as Operation);
            }
        }
    };

    private unmount = (key: number) => {
        if (this.manager) {
            this.manager.unmount(key);
        } else {
            this.queue.push({ type: "unmount", key });
        }
    };

    private nextKey = 0;
    private queue: Operation[] = [];
    private manager: PortalManager | null | undefined;

    render() {
        return (
            <PortalContext.Provider
                value={{
                    mount: this.mount,
                    update: this.update,
                    unmount: this.unmount,
                }}
            >
                {/* Need collapsable=false here to clip the elevations, otherwise they appear above Portal components */}
                <View
                    style={styles.container}
                    collapsable={false}
                    pointerEvents="box-none"
                    testID="portal_host"
                >
                    {this.props.children}
                </View>
                <PortalManager ref={this.setManager} />
            </PortalContext.Provider>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
});
