// TODO: Override the marker.js renderer for urls to enable proper anchor support.

class Docs {
    /**
     * @param {string} defaultView
     * @param {string} routesPath
     */
    constructor(defaultViewPath, fetchPath = 'content', routesPath = './assets/data/routes.json') {
        /** @type {string} */
        this.defaultViewPath = defaultViewPath;

        /** @type {string} */
        this.fetchPath = fetchPath;

        /** @type {string} */
        this.routesPath = routesPath;

        /** @type {Array.<JSON>} */
        this.routesData = [];

        /** @type {Array.<(Route, RoutingNode)} */
        this.routes = [];

        /** @type {Route} */
        this.currentRoute = undefined;

        this.onExec();
    }

    /**
     * Called on initialization by the constructor.
     */
    async onExec() {
        this.routesData = await this.fetchRoutes();

        this.updateRoutes(this.routesData);

        this.generateNav(this.routes, document.querySelector('aside'));

        window.addEventListener('hashchange', () => this.handleLocation(window.location));

        // Check for initial condition.
        if (window.location.hash) {
            // There is a hash in the address bar, load the view.
            this.handleLocation(window.location);
        } else if (typeof this.defaultViewPath !== 'undefined') {
            // TODO: Load a default view.
            console.log('Missing Feature !\nShould load the default view');
        }
    }

    /**
     * Fetch the json file containing the routes.
     * @returns {Array.<JSON>} The array containing the routes.
     */
    async fetchRoutes() {
        const response = await fetch(this.routesPath);
        const json = await response.json();

        return json['routes'];
    }

    /**
     * Update the routes.
     * @param {Array.<JSON>} routesData
     */
    updateRoutes(routesData) {
        const routes = [];

        routesData.forEach(routeData => {
            if ('routes' in routeData) {
                const node = new RoutingNode(
                    routeData['name'],
                    routeData['hidden'],
                    routeData['routes']
                );

                routes.push(node);
            } else {
                const route = new Route(
                    routeData['name'],
                    routeData['hidden'],
                    routeData['filepath']
                );

                routes.push(route);
            }
        });

        this.routes = routes;
    }

    /**
     * Generate the side nav HTML based on routing data.
     * @param {Array.<RoutingItem>} routes
     * @param {HTMLElement} target
     */
    generateNav(routes, target) {
        const nav = document.createElement('nav');

        routes.forEach(routingItem => {
            if (routingItem['hidden'] === false || typeof routingItem['hidden'] === 'undefined') {
                const el = routingItem.buildSelf();

                if (typeof el !== 'undefined') {
                    nav.appendChild(el);
                }
            }
        });

        target.appendChild(nav);
    }

    /**
     * Handle if the view needs to be changed or to move to in-page anchor
     * base on the location data.
     * @param {Location} location
     */
    handleLocation(location) {
        const hashes = location.hash.split('#');
        hashes.splice(0, 1);

        if (typeof this.currentRoute === 'undefined' || this.currentRoute.filepath !== hashes[0]) {
            // TODO: add check in case of no matching routes.
            const route = this.findMatchingRoute(hashes[0], this.routes);

            this.currentRoute = route;

            // Scroll back to top of the content outlet.
            document.querySelector('#router-outlet').scrollTop = 0;

            this.displayRoute(route);

            // Changing classes based on the new active route.
            const activeLinks = document.querySelectorAll('a.active');
            const newActiveLinks = document.querySelectorAll(`a[href='#${hashes[0]}']`);

            if (activeLinks.length > 0) {
                activeLinks.forEach(link => link.classList.remove('active'));
            }

            newActiveLinks.forEach(newLink => newLink.classList.add('active'));

            if (hashes.length > 1) {
                this.handleLocation(window.location);
            }
        } else {
            // Page don't need to be changed, just move to anchor.
            const el = document.querySelector(`#${hashes[1]}`);

            el.scrollIntoView({
                behavior: 'smooth',
                block: 'start'
            });
        }
    }

    /**
     * Recursively search for a matching route based on a filepath.
     * @param {string} filepath The filepath to search.
     * @returns {Route} The matching route.
     */
    findMatchingRoute(filepath, routes) {
        // TODO: Break the loops in a more elegant way.
        let match = undefined;

        for (let routingItem of routes) {
            // If the routingItem is a Route and have what we are looking for.
            if (routingItem.hasOwnProperty('filepath') && routingItem['filepath'] === filepath) {
                match = routingItem;
                break;
            }

            // Recursive search to go deeper.
            for (let v of Object.keys(routingItem)) {
                if (typeof routingItem[v] === 'object' && v === 'routes') {
                    const o = this.findMatchingRoute(filepath, routingItem[v]);

                    if (o != null) {
                        match = o;
                        break;
                    }
                }
            }
        }

        return match;
    }

    /**
     * Load and displays a route's content.
     * @param {Route} route The route to load.
     */
    async displayRoute(route) {
        const content = await route.loadSelf(this.fetchPath);
        const contentOutlet = document.querySelector('#router-outlet');

        this.removeChildren(contentOutlet);
        contentOutlet.innerHTML = marked(content);
        Prism.highlightAll(); // edited
    }

    /**
     * Utilitary function to remove any content inside an HTML element
     * @param {HTMLElement} element
     */
    removeChildren(element) {
        let count = element.childNodes.length;

        while (count--) {
            element.removeChild(element.lastChild);
        }
    }
}

class RoutingItem {
    /**
     * @param {string} name
     * @param {boolean} hidden
     */
    constructor(name, hidden) {
        this.name = name;
        this.hidden = hidden;
    }

    /**
     * @param {*} force Wether to force generation or not despite the hidden property.
     * @returns {HTMLElement} The RoutingItem as an HTML element (div).
     */
    buildSelf(force = false) {
        if (this.hidden === false || typeof this.hidden === 'undefined') {
            force = true;
        }

        if (force) {
            const element = document.createElement('div');

            element.innerText = this.name;

            return element;
        }
    }
}

class Route extends RoutingItem {
    /**
     * @param {string} name
     * @param {boolean} hidden
     * @param {string} filepath
     */
    constructor(name, hidden, filepath) {
        super(name, hidden);

        /** @type {string} */
        this.filepath = filepath;
    }

    /**
     * @param {boolean} force Wether to force generation or not despite the hidden property.
     * @returns {HTMLAnchorElement} The Route as an HTML link.
     */
    buildSelf(force = false) {
        if (this.hidden === false || typeof this.hidden === 'undefined') {
            force = true;
        }

        if (force) {
            const element = document.createElement('a');

            element.href = `#${this.filepath}`;
            element.innerText = this.name;

            return element;
        }
    }

    /**
     * Loads the Route content and returns it as text.
     * @param {string} prefix (optional) An url prefix to add *(i.e: different location)*.
     * @param {string} suffix (optional) An url suffix to add *(i.e: file extension)*.
     * @returns {string} The document content to load as a string.
     */
    async loadSelf(prefix, suffix) {
        if (typeof prefix === 'undefined') {
            prefix = '';
        } else {
            prefix += '/';
        }

        if (typeof suffix === 'undefined') {
            suffix = '';
        }

        const response = await fetch(`${prefix}${this.filepath}${suffix}`);
        const text = await response.text();

        return text;
    }
}

class RoutingNode extends RoutingItem {
    /**
     * @param {string} name
     * @param {boolean} hidden
     * @param {Array.<JSON>} routesData
     */
    constructor(name, hidden, routesData) {
        super(name, hidden);

        /** @type {Array.<JSON>} */
        this.routesData = routesData;

        /** @type {Array.<(Route,RoutingNode)>} */
        this.routes = [];

        this.onExec();
    }

    /**
     * Called on initialization by the constructor.
     */
    onExec() {
        this.routesData.forEach(routeData => {
            if ('routes' in routeData) {
                this.routes.push(new RoutingNode(routeData['name'], routeData['hidden'], routeData['routes']));
            } else {
                this.routes.push(new Route(routeData['name'], routeData['hidden'], routeData['filepath']));
            }
        });
    }

    /**
     * Builds the HTML representation of the RoutingNode as an HTML unorded list.
     * @returns {HTMLUListElement} The RoutingNode as an HTML list.
     */
    buildSelf(force = false) {
        if (this.hidden === false || typeof this.hidden === 'undefined') {
            force = true
        }

        if (force) {
            const element = document.createElement('section');
            element.classList.add('node', 'hide');

            const heading = this.buildSelf_Heading(this.name, toggleDisplay);
            const list = this.buildSelf_List(this.routes);

            element.appendChild(heading);
            element.appendChild(list);

            return element;
        } else {
            return;
        }
    }

    /**
     * @private
     * @param {string} text
     * @param {Function} onClickCallback (optional)
     */
    buildSelf_Heading(text, onClickCallback) {
        const wrapper = document.createElement('div');
        const headingText = document.createElement('span');
        const headingIcon = document.createElement('img');

        headingText.innerText = text;

        headingIcon.src = './assets/img/icons/baseline_expand_more_white_24dp.png';

        wrapper.appendChild(headingText);
        wrapper.appendChild(headingIcon);

        if (typeof onClickCallback === "function") {
            wrapper.addEventListener('click', onClickCallback);
        }

        return wrapper;
    }

    /**
     * @private
     * @param {Array.<(Route,RoutingNode)>} entries
     */
    buildSelf_List(entries) {
        const list = document.createElement('ul');

        entries.forEach(entry => {
            const listItem = document.createElement('li');
            const itemContent = entry.buildSelf();

            if (typeof itemContent !== 'undefined') {
                listItem.appendChild(itemContent);

                list.appendChild(listItem);
            }
        });

        return list;
    }
}

// TODO: find them a new home

/**
 * @param {Event} event
 */
function toggleDisplay(event) {
    const dropdown = event.currentTarget.parentNode;
    // const list = dropdown.querySelector('ul');

    toggleClass(dropdown, 'hide');
};

/**
 * @param {HTMLElement} el
 * @param {string} className
 */
function toggleClass(el, className) {
    if (el.classList.contains(className)) {
        el.classList.remove(className);
    } else {
        el.classList.add(className);
    }
};