import * as React from "react";
import { ReactNode } from "react";
import { Route, RouteProps } from "react-router";
import { url } from "../strings";

interface RouteOptions extends Omit<RouteProps, 'path'> {
    layer?: string;
    /**
     * Generate a key to be added to the Component.
     * - `match_path`: Use the match path as the key - the Component will be recreated whenever the matched segment of the path changes.
     */
    keyBy?: 'match_path' | ((props: any) => string);
}

interface RenderInfo {
    element: ReactNode;
    always_render?: boolean;
}

class RouteEntry {
    constructor(path: string, options: RouteOptions, parent?: RouteEntry) {
        this.relativePath = path;
        this.options = options;
        this.parent = parent;
        if (parent) parent.addEntry(this)
    }

    readonly relativePath: string;
    readonly options: RouteOptions;
    readonly parent: RouteEntry;

    private subEntries: RouteEntry[] = []

    private addEntry(entry: RouteEntry) {
        this.subEntries.push(entry)
    }

    get layer() {
        return this.options.layer || (this.parent && this.parent.layer) || 'default';
    }

    private _path: string;
    get path() {
        if (!this._path) {
            this._path = url`${this.parent ? this.parent.path : ''}/${this.relativePath}`;
        }
        return this._path;
    }

    get hasComponent() {
        return !!(this.options.children || this.options.component || this.options.render);
    }

    protected getAllLayers(layerSet = new Set<string>()) {
        layerSet.add('default');
        if (this.options.layer) layerSet.add(this.options.layer)
        for (let entry of this.subEntries) {
            entry.getAllLayers(layerSet);
        }
        return layerSet;
    }

    protected internalRenderLayer(target_layer: string): RenderInfo {
        const info: RenderInfo = {
            element: null,
            always_render: false,
        }

        if (typeof this.options.children == 'function') info.always_render = true;

        const { layer, keyBy, ...route_props } = this.options;

        const computeKey = (props) => {
            let key = null;
            if (keyBy == 'match_path') {
                key = props.match.url;
                if (!key.endsWith('/')) key += '/';
            } else if (typeof keyBy == 'function') {
                key = keyBy(props);
            }
            return key;
        }

        if (keyBy) {
            if (route_props.component) {
                const Component = route_props.component;
                route_props.component = (props) => {
                    return <Component key={computeKey(props)} {...props} />
                }
            }
            if (route_props.children) {
                if (typeof route_props.children != 'function') {
                    const { children } = route_props;
                    route_props.children = null;
                    route_props.render = (props) => children;
                } else {
                    throw new Error("Cannot use keyBy with children function");
                }
            }
            if (route_props.render) {
                const { render } = route_props;
                route_props.render = (props) => {
                    return <React.Fragment key={computeKey(props)}>{render(props)}</React.Fragment>
                }
            }
        }

        if (this.subEntries.length == 0) {
            if (this.layer != target_layer) return null;
            info.element = <Route key={this.relativePath} path={this.path} {...route_props} />
        } else {
            const subItems: ReactNode[] = [];
            for (let sub of this.subEntries) {
                const subData = sub.internalRenderLayer(target_layer);
                if (subData == null) continue;
                if (subData.always_render) info.always_render = true;
                subItems.push(subData.element);
            }

            if (this.hasComponent && this.layer == target_layer) {
                subItems.push(<Route key='/' path={this.path} {...route_props} />)
            }

            if (subItems.length == 0) return null;

            const childValue = info.always_render ? () => subItems : subItems;
            info.element = <Route key={this.relativePath} path={this.path} {...this.options} children={childValue} />
        }

        return info;
    }

    renderLayerToRoutes(layer: string = 'default') {
        const routes = this.internalRenderLayer(layer).element;
        return routes;
    }

    renderRouteLayers(layers: string[]) {
        const layer_table: Record<string, ReactNode> = {}
        for (let layer of layers) {
            layer_table[layer] = this.renderLayerToRoutes(layer);
        }
        return layer_table;
    }

    renderAllRouteLayers() {
        return this.renderRouteLayers([...this.getAllLayers()])
    }
}

let route_table: RouteEntry;
function normal_route(path: string, options: RouteOptions, block?: () => void) {
    const parent_table = route_table;
    const newEntry = new RouteEntry(path, options, parent_table)
    if (block) {
        route_table = newEntry;
        block();
        route_table = parent_table;
    }
    return newEntry;
}

/**
 * Declaratively build React Router Routes.
 * 
 * Adds the following features:
 * - If a Route on a branch uses `children` (indicating to React Router that it should _always_ render),
 *   all parent routes will be converted to use `children` as well, so that, regardless of where it is used,
 *   it will always render. Ideal for transitioning Modal, Trays, and such.
 * - Route "layers". Allows optionally specifying a layer when specifying a route.
 *   This allows routes to be part of the same tree (and thus inherit relative paths), but to be used in different places.
 *   An example of how this can be used for Modals is included below.
 * 
 * **Example Usage:**
 * ```javascript
 * const ROUTES = route('/', {}, () => {
 *   route('sub/', sub_routes)
 *   route('test_page/', { component: TestPage })
 *   route('test_modal/', { component: TestModal, layer: 'modal' })
 * })
 * const sub_routes = () => {
 *   route('new/', { component: CreatePage });
 * }
 * ```
 * 
 * **Modal Example:**
 * ```javascript
 * const NamedLocationContext = createContext<Record<string, Location>>({})
 * export function useNamedLocation(name: string) {
 *   const ctx = useContext(NamedLocationContext);
 *   return useObserver(() => ctx[name]);
 * }
 * export const NamedLocationRoute: React.SFC<RouteProps & { name: string }> = ({ name, ...pass }) => {
 *   const loc = useNamedLocation(name);
 *   return <Route location={loc} {...pass} />
 * }
 * export function ModalSwitch({ children }) {
 *   const location = useLocation<any>();
 *   const background = location.state && location.state.background;
 * 
 *   const backgroundLocation = background || location;
 *   const namedLocations = useAsObservableSource({
 *     modal: background ? location : null,
 *   });
 * 
 *   return (
 *     <NamedLocationContext.Provider value={namedLocations}>
 *       <Route location={backgroundLocation}>
 *         {children}
 *       </Route>
 *     </NamedLocationContext.Provider>
 *   );
 * }
 * const ROUTE_LAYERS = ROUTES.renderAllRouteLayers();
 * const WRAPPED_ROUTES = Object.entries(ROUTE_LAYERS).map(([layer, routes]) => {
 *   if (name == 'default') return routes;
 *   return <NamedLocationRoute key={layer} name={layer} children={routes} />
 * })
 * const RoutesComponent = () => {
 *   return <ModalSwitch children={WRAPPED_ROUTES} />
 * }
 * ```
 * 
 * @param path 
 * @param block 
 */
export function route(path: string, block: () => void): RouteEntry;
export function route(path: string, options: RouteOptions, block?: () => void): RouteEntry;
export function route(a, b, c?) {
    if (typeof b == 'function') {
        return normal_route(a, {}, b);
    } else {
        return normal_route(a, b, c);
    }
}
