declare module StringFilr {
    export interface IStringFilrSettings {
        /**
         * An Object containing data stored as children of sub-Objects.
         */
        library: any;

        /**
         * A String to use as a default key to rescue on, if provided.
         */
        normal?: string;

        /**
         * Whether it's ok for the library to have Objects that don't contain the
         * normal key (by default, false).
         */
        requireNormalKey?: boolean;
    }

    export interface IStringFilr {
        getLibrary(): any;
        getNormal(): string;
        getCache(): any;
        getCached(key: string): any;
        clearCache(): void;
        clearCached(key: string): void;
        get(keyRaw: string): any;
    }
}

module StringFilr {
    "use strict";

    /**
     * A general utility for retrieving data from an Object based on nested class
     * names. You can think of the internal "library" Object as a tree structure,
     * such that you can pass in a listing (in any order) of the path to data for 
     * retrieval.
     */
    export class StringFilr implements IStringFilr {
        // The library of data.
        private library: any;

        // Listing of previously found lookups, for speed's sake.
        private cache: any;

        // Optional default class to use when no suitable option is found.
        private normal: string;

        // Whether to crash when a sub-object in reset has no normal child.
        private requireNormalKey: boolean;

        /**
         * @param {IStringFilrSettings} settings
         */
        constructor(settings: IStringFilrSettings) {
            if (!settings) {
                throw new Error("No settings given to StringFilr.");
            }

            if (!settings.library) {
                throw new Error("No library given to StringFilr.");
            }
            this.library = settings.library;

            this.normal = settings.normal;
            this.requireNormalKey = settings.requireNormalKey;

            this.cache = {};

            if (this.requireNormalKey) {
                if (typeof this.normal === "undefined") {
                    throw new Error("StringFilr is given requireNormalKey, but no normal class.");
                }

                this.ensureLibraryNormal();
            }
        }

        /**
         * @return {Object} The base library of stored information.
         */
        getLibrary(): any {
            return this.library;
        }

        /**
         * @return {String} The optional normal class String.
         */
        getNormal(): string {
            return this.normal;
        }

        /**
         * @return {Object} The complete cache of cached output.
         */
        getCache(): any {
            return this.cache;
        }

        /**
         * @return {Mixed} A cached value, if it exists/
         */
        getCached(key: string): any {
            return this.cache[key];
        }

        /**
         * Completely clears the cache Object.  
         */
        clearCache(): void {
            this.cache = {};
        }

        /**
         * Clears the cached entry for a key.
         * 
         * @param {String} key
         */
        clearCached(key: string): void {
            delete this.cache[key];

            if (this.normal) {
                delete this.cache[key.replace(this.normal, "")];
            }
        }

        /**
         * Retrieves the deepest matching data in the library for a key. 
         * 
         * @param {String} keyRaw
         * @return {Mixed}
         */
        get(keyRaw: string): any {
            var key: string,
                result: any;

            if (this.normal) {
                key = keyRaw.replace(this.normal, "");
            } else {
                key = keyRaw;
            }

            // Quickly return a cached result if it exists
            if (this.cache.hasOwnProperty(key)) {
                return this.cache[key];
            }

            // Since no existed, it must be found deep within the library
            result = this.followClass(key.split(/\s+/g), this.library);

            this.cache[key] = this.cache[keyRaw] = result;
            return result;
        }

        /**
         * Utility helper to recursively check for tree branches in the library 
         * that don't have a key equal to the normal. For each sub-directory that
         * is caught, the path to it is added to output.
         * 
         * @param {Object} current   The current location being searched within
         *                           the library.
         * @param {String} path   The current path within the library.
         * @param {String[]} output   An Array of the String paths to parts that
         *                           don't have a matching key.
         * @return {String[]} output
         */
        private findLackingNormal(current: any, path: string, output: string[]): string[] {
            var i: string;

            if (!current.hasOwnProperty(this.normal)) {
                output.push(path);
            }

            if (typeof current[i] === "object") {
                for (i in current) {
                    if (current.hasOwnProperty(i)) {
                        this.findLackingNormal(current[i], path + " " + i, output);
                    }
                }
            }

            return output;
        }

        /**
         * Utility function to follow a path into the library (this is the driver 
         * for searching into the library). For each available key, if it matches
         * a key in current, it is removed from keys and recursion happens on the
         * sub-directory in current.
         * 
         * @param {String[]} keys   The currently available keys to search within.
         * @param {Object} current   The current location being searched within
         *                           the library.
         * @return {Mixed} The most deeply matched part of the library.
         */
        private followClass(keys: string[], current: any): any {
            var key: string,
                i: number;

            // If keys runs out, we're done
            if (!keys || !keys.length) {
                return current;
            }

            // For each key in the current array...
            for (i = 0; i < keys.length; i += 1) {
                key = keys[i];

                // ...if it matches, recurse on the other keys
                if (current.hasOwnProperty(key)) {
                    keys.splice(i, 1);
                    return this.followClass(keys, current[key]);
                }
            }

            // If no key matched, try the normal (default)
            if (this.normal && current.hasOwnProperty(this.normal)) {
                return this.followClass(keys, current[this.normal]);
            }

            // Nothing matches anything; we're done.
            return current;
        }

        /**
         * Driver for this.findLackingNormal. If library directories are found to
         * not have a normal, it throws an error.
         */
        private ensureLibraryNormal(): void {
            var caught: string[] = this.findLackingNormal(this.library, "base", []);

            if (caught.length) {
                throw new Error("Found " + caught.length + " library "
                    + "sub-directories missing the normal: "
                    + "\r\n  " + caught.join("\r\n  "));
            }
        }
    }
}
