declare module ChangeLinr {
    export interface IChangeLinrTransform {
        (data: any, key: string, attributes: any, scope: IChangeLinr): any;
    }

    export interface IChangeLinrCache {
        [i: string]: any;
    }

    export interface IChangeLinrCacheFull {
        [i: string]: {
            [i: string]: any;
        }
    }

    export interface IChangeLinrSettings {
        pipeline: string[];
        transforms: {
            [i: string]: IChangeLinrTransform
        };
        doMakeCache?: boolean;
        doUseCache?: boolean;
    }

    export interface IChangeLinr {
        getCache(): IChangeLinrCache;
        getCached(key: string): any;
        getCacheFull(): IChangeLinrCacheFull;
        getDoMakeCache(): boolean;
        getDoUseCache(): boolean;
        process(data: any, key?: string, attributes?: any): any;
        processFull(data: any, key?: string, attributes?: any): any;
    }
}


module ChangeLinr {
    "use strict";

    /**
     * A general utility for transforming raw input to processed output. This is
     * done by keeping an Array of transform Functions to process input on.
     * Outcomes for inputs are cached so repeat runs are O(1).
     */
    export class ChangeLinr implements IChangeLinr {
        /**
         * Functions that may be used to transform data, keyed by name.
         */
        private transforms: {
            [i: string]: IChangeLinrTransform;
        };

        /**
         * Ordered listing of Function names to be applied to raw input.
         */
        private pipeline: string[];

        /**
         * Cached output of previous results of the the pipeline.
         */
        private cache: IChangeLinrCache;

        /**
         * Cached output of each step of the pipeline.
         */
        private cacheFull: IChangeLinrCacheFull;

        /**
         * Whether this should be caching responses.
         */
        private doMakeCache: boolean;

        /**
         * Whether this should be retrieving and using cached results.
         */
        private doUseCache: boolean;

        /**
         * @param {IChangeLinrSettings} settings
         */
        constructor(settings: IChangeLinrSettings) {
            var i: number;

            if (typeof settings.pipeline === "undefined") {
                throw new Error("No pipeline given to ChangeLinr.");
            }
            this.pipeline = settings.pipeline || [];

            if (typeof settings.transforms === "undefined") {
                throw new Error("No transforms given to ChangeLinr.");
            }
            this.transforms = settings.transforms || {};

            this.doMakeCache = typeof settings.doMakeCache === "undefined"
                ? true : settings.doMakeCache;

            this.doUseCache = typeof settings.doUseCache === "undefined"
                ? true : settings.doUseCache;

            this.cache = {};
            this.cacheFull = {};

            // Ensure the pipeline is formatted correctly
            for (i = 0; i < this.pipeline.length; ++i) {
                // Don't allow null/false transforms
                if (!this.pipeline[i]) {
                    throw new Error("Pipe[" + i + "] is invalid.");
                }

                // Make sure each part of the pipeline exists
                if (!this.transforms.hasOwnProperty(this.pipeline[i])) {
                    if (!this.transforms.hasOwnProperty(this.pipeline[i])) {
                        throw new Error(
                            "Pipe[" + i + "] (\"" + this.pipeline[i] + "\") "
                            + "not found in transforms."
                            );
                    }
                }

                // Also make sure each part of the pipeline is a Function
                if (!(this.transforms[this.pipeline[i]] instanceof Function)) {
                    throw new Error(
                        "Pipe[" + i + "] (\"" + this.pipeline[i] + "\") "
                        + "is not a valid Function from transforms."
                        );
                }

                this.cacheFull[i] = this.cacheFull[this.pipeline[i]] = {};
            }
        }


        /* Simple gets
        */

        /**
         * @return {Mixed} The cached output of this.process and this.processFull.
         */
        getCache(): IChangeLinrCache  {
            return this.cache;
        }

        /**
         * @param {String} key   The key under which the output was processed
         * @return {Mixed} The cached output filed under the given key.
         */
        getCached(key: string): any {
            return this.cache[key];
        }

        /**
         * @return {Object} A complete listing of the cached outputs from all 
         *                  processed information, from each pipeline transform.
         */
        getCacheFull(): IChangeLinrCacheFull {
            return this.cacheFull;
        }

        /**
         * @return {Boolean} Whether the cache object is being kept.
         */
        getDoMakeCache(): boolean {
            return this.doMakeCache;
        }

        /**
         * @return {Boolean} Whether previously cached output is being used in new
         *                   process requests.
         */
        getDoUseCache(): boolean {
            return this.doUseCache;
        }


        /* Core processing
        */

        /**
         * Applies a series of transforms to input data. If doMakeCache is on, the
         * outputs of this are stored in cache and cacheFull.
         * 
         * @param {Mixed} data   The data to be transformed.
         * @param {String} [key]   They key under which the data is to be stored.
         *                         If needed but not provided, defaults to data.
         * @param {Object} [attributes]   Any extra attributes to be given to the
         *                                transform Functions.
         * @return {Mixed} The final output of the pipeline.
         */
        process(data: any, key: string = undefined, attributes: any = undefined): any {
            var i: number;

            if (typeof key === "undefined" && (this.doMakeCache || this.doUseCache)) {
                key = data;
            }

            // If this keyed input was already processed, get that
            if (this.doUseCache && this.cache.hasOwnProperty(key)) {
                return this.cache[key];
            }

            // Apply (and optionally cache) each transform in order
            for (i = 0; i < this.pipeline.length; ++i) {
                data = this.transforms[this.pipeline[i]](data, key, attributes, this);

                if (this.doMakeCache) {
                    this.cacheFull[this.pipeline[i]][key] = data;
                }
            }

            if (this.doMakeCache) {
                this.cache[key] = data;
            }

            return data;
        }

        /**
         * A version of this.process that returns the complete output from each 
         * pipelined transform Function in an Object.
         * 
         * @param {Mixed} data   The data to be transformed.
         * @param {String} [key]   They key under which the data is to be stored.
         *                         If needed but not provided, defaults to data.
         * @param {Object} [attributes]   Any extra attributes to be given to the
         *                                transform Functions.
         * @return {Object} The complete output of the transforms.
         */
        processFull(raw: any, key: string, attributes: any = undefined): any {
            var output: any = {},
                i: number;

            this.process(raw, key, attributes);

            for (i = 0; i < this.pipeline.length; ++i) {
                output[i] = output[this.pipeline[i]] = this.cacheFull[this.pipeline[i]][key];
            }

            return output;
        }
    }
}
