"use strict";

// modified page.js by visionmedia
// - changed regexes to components
// - removed unused crap
// - refactored to classes
// - simplified method chains
// - added ability to call .save() in .exit() without side effects
// - page refresh recovers state from history
// - rename .save() to .replaceState()
// - offer .url

const clickEvent = document.ontouchstart ? "touchstart" : "click";
const uri = require("./util/uri.js");
let location = window.history.location || window.location;

function _getOrigin() {
    return (
        location.protocol +
        "//" +
        location.hostname +
        (location.port ? ":" + location.port : "")
    );
}

function _isSameOrigin(href) {
    return href && href.indexOf(_getOrigin()) === 0;
}

function _getBaseHref() {
    const bases = document.getElementsByTagName("base");
    return bases.length > 0
        ? bases[0].href.replace(_getOrigin(), "").replace(/\/+$/, "")
        : "";
}

class Context {
    constructor(path, state) {
        const base = _getBaseHref();
        path = path.indexOf("/") !== 0 ? "/" + path : path;
        path = path.indexOf(base) !== 0 ? base + path : path;

        this.canonicalPath = path;
        this.path = !path.indexOf(base) ? path.slice(base.length) : path;

        this.title = document.title;
        this.state = state || {};
        this.state.path = path;
        this.parameters = {};
    }

    pushState() {
        history.pushState(this.state, this.title, this.canonicalPath);
    }

    replaceState() {
        history.replaceState(this.state, this.title, this.canonicalPath);
    }
}

class Route {
    constructor(path) {
        this.method = "GET";
        this.path = path;

        this.parameterNames = [];
        if (this.path === null) {
            this.regex = /.*/;
        } else {
            let parts = [];
            for (let component of this.path) {
                if (component[0] === ":") {
                    parts.push("([^/]+)");
                    this.parameterNames.push(component.substr(1));
                } else {
                    // assert [a-z]+
                    parts.push(component);
                }
            }
            let regexString = "^/" + parts.join("/");
            regexString += "(?:/*|/((?:(?:[a-z]+=[^/]+);)*(?:[a-z]+=[^/]+)))$";
            this.parameterNames.push("variable");
            this.regex = new RegExp(regexString);
        }
    }

    middleware(fn) {
        return (ctx, next) => {
            if (this.match(ctx.path, ctx.parameters)) {
                return fn(ctx, next);
            }
            next();
        };
    }

    match(path, parameters) {
        const qsIndex = path.indexOf("?");
        const pathname = ~qsIndex ? path.slice(0, qsIndex) : path;
        const match = this.regex.exec(pathname);

        if (!match) {
            return false;
        }

        try {
            for (let i = 1; i < match.length; i++) {
                const name = this.parameterNames[i - 1];
                const value = match[i];
                if (value === undefined) {
                    continue;
                }

                if (name === "variable") {
                    for (let word of (value || "").split(/;/)) {
                        const [key, subvalue] = word.split(/=/, 2);
                        parameters[key] = uri.unescapeParam(subvalue);
                    }
                } else {
                    parameters[name] = uri.unescapeParam(value);
                }
            }
        } catch (e) {
            return false;
        }

        return true;
    }
}

class Router {
    constructor() {
        this._callbacks = [];
        this._exits = [];
    }

    enter(path) {
        const route = new Route(path);
        for (let i = 1; i < arguments.length; ++i) {
            this._callbacks.push(route.middleware(arguments[i]));
        }
    }

    exit(path, fn) {
        const route = new Route(path);
        for (let i = 1; i < arguments.length; ++i) {
            this._exits.push(route.middleware(arguments[i]));
        }
    }

    start() {
        if (this._running) {
            return;
        }
        this._running = true;
        this._onPopState = _onPopState(this);
        this._onClick = _onClick(this);
        window.addEventListener("popstate", this._onPopState, false);
        document.addEventListener(clickEvent, this._onClick, false);
        const url = location.pathname + location.search + location.hash;
        return this.replace(url, history.state, true);
    }

    stop() {
        if (!this._running) {
            return;
        }
        this._running = false;
        document.removeEventListener(clickEvent, this._onClick, false);
        window.removeEventListener("popstate", this._onPopState, false);
    }

    showNoDispatch(path, state) {
        const ctx = new Context(path, state);
        ctx.pushState();
        this.ctx = ctx;
        return ctx;
    }

    show(path, state, push) {
        const ctx = new Context(path, state);
        const oldPath = this.ctx ? this.ctx.path : ctx.path;
        this.dispatch(ctx, () => {
            if (ctx.path !== oldPath && push !== false) {
                ctx.pushState();
            }
        });
        return ctx;
    }

    replace(path, state, dispatch) {
        var ctx = new Context(path, state);
        if (dispatch) {
            this.dispatch(ctx, () => {
                ctx.replaceState();
            });
        } else {
            ctx.replaceState();
        }
        return ctx;
    }

    dispatch(ctx, middle) {
        const swap = (_ctx, next) => {
            this.ctx = ctx;
            middle();
            next();
        };
        const callChain = (this.ctx ? this._exits : []).concat(
            [swap],
            this._callbacks,
            [this._unhandled, (ctx, next) => {}]
        );

        let i = 0;
        let fn = () => {
            callChain[i++](this.ctx, fn);
        };
        fn();
    }

    _unhandled(ctx, next) {
        let current = location.pathname + location.search;
        if (current === ctx.canonicalPath) {
            return;
        }
        this.stop();
        location.href = ctx.canonicalPath;
    }

    get url() {
        return location.pathname + location.search + location.hash;
    }
}

const _onPopState = (router) => {
    let loaded = false;
    if (document.readyState === "complete") {
        loaded = true;
    } else {
        window.addEventListener("load", () => {
            setTimeout(() => {
                loaded = true;
            }, 0);
        });
    }
    return (e) => {
        if (!loaded) {
            return;
        }
        if (e.state) {
            const path = e.state.path;
            router.replace(path, e.state, true);
        } else {
            router.show(location.pathname + location.hash, undefined, false);
        }
    };
};

const _onClick = (router) => {
    return (e) => {
        if (1 !== _which(e)) {
            return;
        }
        if (e.metaKey || e.ctrlKey || e.shiftKey) {
            return;
        }
        if (e.defaultPrevented) {
            return;
        }

        let el = e.path ? e.path[0] : e.target;
        while (el && el.nodeName !== "A") {
            el = el.parentNode;
        }
        if (!el || el.nodeName !== "A") {
            return;
        }

        if (
            el.hasAttribute("download") ||
            el.getAttribute("rel") === "external"
        ) {
            return;
        }

        const link = el.getAttribute("href");
        if (el.pathname === location.pathname && (el.hash || "#" === link)) {
            return;
        }
        if (link && link.indexOf("mailto:") > -1) {
            return;
        }
        if (el.target) {
            return;
        }
        if (!_isSameOrigin(el.href)) {
            return;
        }

        const base = _getBaseHref();
        const orig = el.pathname + el.search + (el.hash || "");
        const path = !orig.indexOf(base) ? orig.slice(base.length) : orig;

        if (base && orig === path) {
            return;
        }

        e.preventDefault();
        router.show(orig);
    };
};

function _which(e) {
    e = e || window.event;
    return e.which === null ? e.button : e.which;
}

Router.prototype.Context = Context;
Router.prototype.Route = Route;
module.exports = new Router();