import { IgniteHtml, IgniteRendering } from "../ignite-html/ignite-html.js";
import { IgniteElement } from "../ignite-html/ignite-element.js";
import { IgniteTemplate, slot, div, html } from "../ignite-html/ignite-template.js";
import { IgniteCallback, IgniteProperty} from "../ignite-html/ignite-html.js";

/**
 * Creates a route listener that runs when a route is switched. The listener will run on construct the first time and update the state.
 * @class IgniteTemplate
 * @memberof IgniteTemplate
 * @param {String,String[]} routes A single or multiple set of routes that will invoke the callback if met.
 * @param {Function(route, data)} showCallback A callback function that is called when the route is shown. Default is null.
 * @param {Function} hideCallback A callback function that is called when the route is hidden. Default is null.
 * @param {Boolean} strict If true all routes must match before running the callback. Default is false.
 * @returns {IgniteTemplate} This ignite template.
 */
IgniteTemplate.prototype.route = function(routes, showCallback = null, hideCallback = null, strict = false) {
    //If routes is not an array, wrap it.
    if (!Array.isArray(routes)) {
        routes = [routes];
    }

    //By default hide this route.
    this.element.style.setProperty("display", "none", "important");
    
    //Create an update method that will be used to check and see if the route is met.
    var update = (event) => {
        var routeMatches = false;
        var matchedRoute = null;

        //Create an object to hold any data.
        var data = {};

        //Based on whether we are strict matching or not check if we have a match.
        if (!strict) {
            for (var i = 0; i < routes.length && !routeMatches; i++) {
                routeMatches = Router.matches(routes[i], data);
                matchedRoute = routes[i];
            }
        } else {
            routeMatches = true;
            for (var i = 0; i < routes.length && routeMatches; i++) {
                routeMatches = Router.matches(routes[i], data);
                matchedRoute = routes[i];
            }
        }

        //Invoke the callback if the route matched.
        if (routeMatches) {
            //Show the route element.
            this.element.style.removeProperty("display");

            if (showCallback) {
                showCallback(matchedRoute, (event && event.data ? event.data : data));
            }
        } else {
            //Hide the route element.
            this.element.style.setProperty("display", "none", "important");

            if (hideCallback) {
                hideCallback();
            }
        }
    };

    //Create a managed push & pop callback
    var managedPush = new IgniteCallback(update, () => {
        window.removeEventListener("pushstate", managedPush.callback);
    });

    var managedPop = new IgniteCallback(update, () => {
        window.removeEventListener("popstate", managedPop.callback);
    });

    //Register our push & pop callbacks.
    window.addEventListener("pushstate", managedPush.callback);
    window.addEventListener("popstate", managedPop.callback);

    //Add the managed callbacks to our template so that upon deconstruction our callback is destroyed correctly.
    this._callbacks.push(managedPush);
    this._callbacks.push(managedPop);

    //Upon ready call update so our view is displayed if needed.
    IgniteRendering.ready(update);

    return this;
};

/**
 * An extension that navigates to a given route when a IgniteTemplate is clicked.
 * @param {String|IgniteProperty|Function} route The route to navigate to. 
 * @param {Any|IgniteProperty|Function} data The data to pass along with this navigate, this only applies when not refreshing, default is null.
 * @param {Boolean|IgniteProperty|Function} refresh Whether or not to refresh the page, default is false.
 * @param {Boolean|IgniteProperty|Function} keepQuery Whether or not to keep the current url query, default is false.
 * @returns {IgniteTemplate} This ignite template.
 */
IgniteTemplate.prototype.navigate = function(route, data = null, refresh = false, keepQuery = false) {
    return this.onClick((e) => {
        Router.navigate(
            (route instanceof IgniteProperty ? route.value : (route instanceof Function ? route() : route)),
            (data instanceof IgniteProperty ? data.value : (data instanceof Function ? data() : data)),
            (refresh instanceof IgniteProperty ? refresh.value : (refresh instanceof Function ? refresh() : refresh)),
            (keepQuery instanceof IgniteProperty ? keepQuery.value : (keepQuery instanceof Function ? keepQuery() : keepQuery))
        );

        e.preventDefault();
        e.stopPropagation();
        return false;
    });
}

class RouterLink extends IgniteElement {
    constructor() {
        super();

        this.pushStateListener = () => this.update();
        this.popStateListener = () => this.update();

        window.addEventListener("popstate", this.popStateListener);
        window.addEventListener("pushstate", this.pushStateListener);
    }

    get properties() {
        return {
            disabled: false,
            active: false,
            routes: [],
            target: null
        };
    }

    get styles() {
        return /*css*/`
            router-link {
                display: flex;
                flex-direction: row;
                align-items: center;
                justify-content: center;
            }
        `;
    }

    render() {
        return this.template
            .onClick((event) => this.onClick(event))
            .class(this.active, value => value ? "active" : null)
            .child(new slot(this).class(this.active, value => value ? "active" : null));
    }

    ready() {
        this.update();
    }

    update() {
        var routeMatches = false;

        //Check the target first.
        routeMatches = Router.matches(this.target);

        //Check optional routes next.
        for (var i = 0; i < this.routes.length && !routeMatches; i++) {
            routeMatches = Router.matches(this.routes[i]);
        }

        if (routeMatches && !this.active) {
            this.active = true;
        } else if (!routeMatches && this.active) {
            this.active = false;
        }
    }

    onClick(event) {
        if (!this.disabled) {
            event.preventDefault();
            Router.navigate(this.target, null, false);
        }
    }

    cleanup() {
        window.removeEventListener("popstate", this.popStateListener);
        window.removeEventListener("pushstate", this.pushStateListener);
    }
}

class RouterView extends IgniteElement {
    constructor() {
        super();

        this.pushStateListener = () => this.update();
        this.popStateListener = () => this.update();

        window.addEventListener("popstate", this.popStateListener);
        window.addEventListener("pushstate", this.pushStateListener);
    }

    get properties() {
        return {
            show: false,
            routes: [],
            strict: false
        };
    }

    render() {
        return this.template
            .child(new slot(this))
            .style("display", this.show, null, value => value ? null : "none");
    }

    ready() {
        this.update();
    }

    update() {
        var routeMatches = false;

        //Based on whether we are strict matching or not check if we have a match.
        if (!this.strict) {
            for (var i = 0; i < this.routes.length && !routeMatches; i++) {
                routeMatches = Router.matches(this.routes[i]);
            }
        } else {
            routeMatches = true;
            for (var i = 0; i < this.routes.length && routeMatches; i++) {
                routeMatches = Router.matches(this.routes[i]);
            }
        }

        //If we found a match show this router view if it's not already visible, otherwise hide it.
        if (routeMatches && !this.show) {
            this.show = true;
        } else if (!routeMatches && this.show) {
            this.show = false;
        }
    }

    cleanup() {
        window.removeEventListener("popstate", this.popStateListener);
        window.removeEventListener("pushstate", this.pushStateListener);
    }
}

class RouterLinkTemplate extends IgniteTemplate {
    /**
     * Initializes a new router link template.
     * @param {String} target The target route when the link is clicked.
     * @param {String|String[]} routes Optional routes that can be used to control the active state of the link.
     * @param  {...any} elements Elements to render within the link.
     */
    constructor(target, routes, ...elements) {
        super("router-link", elements);

        if (!routes) {
            routes = [];
        }

        if (!Array.isArray(routes)) {
            routes = [routes];
        }

        this.property("target", target);
        this.property("routes", routes);
    }
}

class RouterViewTemplate extends IgniteTemplate {
    /**
     * Initializes a new router view.
     * @param {String|String[]} routes Single or multiple routes to trigger this view to render. 
     * @param {any|any[]} elements Elements to render within the view.
     * @param {Boolean} strict If true all routes must match before this view becomes visible.
     */
    constructor(routes, elements, strict = false) {
        super("router-view", Array.isArray(elements) ? elements : [elements]);

        if (!Array.isArray(routes)) {
            routes = [routes];
        }

        this.property("routes", routes);
        this.property("strict", strict);
    }
}

class Router {
    /**
     * Navigates to a given route and refreshes the page if requested.
     * @param {String} route The route to navigate to.
     * @param {Any} data The data to pass along with this navigate, this only applies when not refreshing, default is null.
     * @param {Boolean} refresh Whether or not to refresh the page, default is false.
     * @param {Boolean} keepQuery Whether or not to keep the current url query, default is false.
     */
    static navigate(route, data = null, refresh = false, keepQuery = false) {
        //If keepQuery is true then include the current query.
        if (keepQuery) {
            if (route.includes("?")) {
                route = route.split("?")[0] + window.location.search;
            } else {
                route = route + window.location.search;
            }
        }

        if (refresh) {
            if (Router.hashMode) {
                //In hash mode the route can't start with / or #, we have to handle it here.
                if (route.startsWith("/") || route.startsWith("#")) {
                    route = route.substr(1);
                }

                window.location.hash = route;
                window.location.reload();
            } else {
                window.location.href = route;
            }
        } else {
            if (Router.hashMode) {
                //In hash mode the route can't start with / or #, we have to handle it here.
                if (route.startsWith("/") || route.startsWith("#")) {
                    route = route.substr(1);
                }

                window.location.hash = route;
            } else {
                window.history.pushState(route, route, route);
            }

            //Push the route to our internal states
            Router.states.push(route);

            //Create a new pushstate event and fire it.
            var event = new Event("pushstate");
            event.data = data;
            window.dispatchEvent(event);
        }
    }

    /**
     * Navigates back one with the ability to have a fallback route or refresh the page.
     * @param {String} fallback The fallback route incase there is no history, default is null.
     * @param {Boolean} refresh Whether or not to refresh the page, default is false.
     */
    static back(fallback = null, refresh  = false) {
        if (Router.states && Router.states.length > 1) {
            Router.states.pop(); //Pop the current state.
            Router.navigate(Router.states.pop(), null, refresh); //Navigate to the previous state.
        } else {
            //Pop the current state, otherwise it can be used to go back but we don't want that.
            if (Router.states.length > 0) {
                Router.states.pop();
            }

            if (fallback) {
                Router.navigate(fallback, null, refresh);
            } else {
                window.history.back();
            }
        }
    }   

    /**
     * Returns whether or not the current location matches a given route.
     * @param {String} route The route to check.
     * @param {Object} data The object to be populated with data from the route.
     * @returns {Boolean} Returns whether or not the current browser location matches the route.
     * @example matches('/user/**') //Anything after /user/ is considered a match.
     * @example matches('/user/{id}/profile') //Anything between /user and /profile is a match. data.id would be set to the value inside {id}
     * @example matches('/user/!0/profile') //Anything between /user and /profile that is not '0' is a match.
     */
    static matches(route, data = null) {
        //Get the path parts from the window
        var pathParts = [];

        //If hash mode is set and we have a hash location, get it and split it.
        if (Router.hashMode && window.location.hash && window.location.hash.length > 0) {
            var path = window.location.hash.substr(1);

            //If the path contains ? then remove the query.
            if (path.includes("?")) {
                path = path.split("?")[0];
            }

            //Break the path into path parts.
            pathParts = path.split("/");
            if (pathParts.length > 0 && pathParts[0].length == 0) {
                pathParts.splice(1);
            }
        }
        else if (!Router.hashMode) {
            pathParts = window.location.pathname.split("/").splice(1);
        }

        //Get the route parts
        var fromRoot = (route.trim().startsWith("/"));
        var routeParts = (fromRoot ? route.trim().split("/").splice(1) : route.trim().split("/"));

        //Check to see if we have a trailing route part, if so, remove it.
        if (pathParts.length > 0 && pathParts[pathParts.length - 1] == "") {
            pathParts.pop();
        }

        if (routeParts.length > 0 && routeParts[routeParts.length - 1] == "") {
            routeParts.pop();
        }

        //If path parts is 0 and route parts is 0 we have a match.
        if (pathParts.length == 0 && routeParts.length == 0) {
            return true;
        }
        //If path parts is 0, and the route part is ** then this is a match.
        else if (pathParts.length == 0 && routeParts.length == 1 && (routeParts[0] == "**" || routeParts[0] == "*")) {
            return true;
        }
        //If path parts is 0 and the route starts with ! and is a length of 1 then this is a match.
        else if (pathParts.length == 0 && routeParts.length == 1 && routeParts[0].startsWith("!")) {
            return true;
        }
        //If the path parts is 0 and the route is !*/** then this is a match
        else if (pathParts.length == 0 && routeParts.length == 2 && routeParts[0].startsWith("!") && routeParts[1] == "**") {
            return true;
        }

        //Check the route parts against the path parts.
        var max = Math.min(pathParts.length, routeParts.length);
        if (fromRoot) {
            for (var i = 0; i < max; i++) {
                if (routeParts[i].startsWith("{") && routeParts[i].endsWith("}")) {
                    if (data) {
                        data[routeParts[i].substring(1, routeParts[i].length - 1)] = pathParts[i];
                    }
                } else if (routeParts[i].startsWith("!") && pathParts[i] == routeParts[i].substring(1)) {
                    return false;
                } else if (routeParts[i] == "**") {
                    return true;
                } else if (routeParts[i] != pathParts[i] && routeParts[i] != "*" && !routeParts[i].startsWith("!")) {
                    return false;
                } else if (i + 2 == routeParts.length && i + 1 == pathParts.length && routeParts[i + 1] == "**") {
                    return true;
                }
            }

            if (routeParts.length > pathParts.length) {
                return false;
            } else if (pathParts.length > routeParts.length && routeParts[routeParts.length - 1] != "**") {
                return false;
            } else {
                return true;
            }
        } else {
            for (var offset = 0; offset < pathParts.length; offset++) {
                for (var i = 0; i < max; i++) {
                    if (i + offset >= pathParts.length) {
                        return false;
                    }
                    else if (routeParts[i].startsWith("!") && pathParts[i + offset] == routeParts[i].substring(1)) {
                        break;
                    } else if (routeParts[i] == "**") {
                        return true;
                    }
                    else if (routeParts[i] != pathParts[i + offset] && routeParts[i] != "*" && !routeParts[i].startsWith("!")) {
                        break;
                    } else if (i + 1 == routeParts.length && offset + routeParts.length == pathParts.length) {
                        return true;
                    } else if (i + 2 == routeParts.length && offset + i + 1 == pathParts.length && routeParts[i + 1] == "**") {
                        return true;
                    }
                }
            }

            return false;
        }
    }

    /**
     * Returns a query parameter from the route search if there is one by it's name.
     * @param {String} name The name of the query parameter to get.
     * @returns {String} Null if not found, or the value of the query parameter.
     */
    static getParameter(name) {
        if (Router.hashMode) {
            var params = new URLSearchParams(window.location.hash.includes("?") ? "?" + window.location.hash.split("?")[1] : "");
            return (params.has(name) ? params.get(name) : null); 
        } else {
            var params = new URLSearchParams(window.location.search);
            return (params.has(name) ? params.get(name) : null);
        }
    }

    /**
     * Removes a query parameter from the route search if it exists.
     * @param {String} name 
     */
    static removeParameter(name) {
        if (Router.hashMode) {
            var params = new URLSearchParams(window.location.hash.includes("?") ? "?" + window.location.hash.split("?")[1] : "");
            if (params.has(name)) {
                params.delete(name);
                window.location.hash = window.location.hash.split("?")[0] + "?" + params.toString();
            }
        } else {
            var url = new URL(window.location);
            url.searchParams.delete(name);
            window.history.pushState(null, '', url.toString());
        }
    }
}

Router.states = [`${window.location.pathname}${window.location.search}${window.location.hash}`];
Router.hashMode = false;

IgniteHtml.register("router-link", RouterLink);
IgniteHtml.register("router-view", RouterView);

window.Router = Router;

export {
    RouterLinkTemplate as RouterLink,
    RouterViewTemplate as RouterView,
    Router
}