/// <reference path="InputWritr-0.2.0.ts" />

declare module DeviceLayr {
    /**
     * 
     */
    export interface IGamepad {
        axes: number[];
        buttons: IGamepadButton[];
        mapping: string;
    }

    /**
     * 
     */
    export interface IGamepadButton {
        value: number;
        pressed: boolean;
    }

    /**
     * 
     */
    export interface ITriggers {
        [i: string]: IButtonListing | IJoystickListing;
    }

    /**
     * 
     */
    export interface IButtonListing {
        status?: boolean;
        trigger: string;
    }

    /**
     * 
     */
    export interface IJoystickListing {
        [i: string]: IJoystickTriggerAxis;
    }

    /**
     * 
     */
    export interface IJoystickTriggerAxis {
        negative: string;
        positive: string;
        status?: AxisStatus;
    }

    /**
     * 
     */
    export interface IAliases {
        on: string;
        off: string;
    }

    /**
     * 
     */
    export interface IControllerMappings {
        [i: string]: IControllerMapping;
    }

    /**
     * 
     */
    export interface IControllerMapping {
        axes: IAxisSchema[];
        buttons: string[];
        joystickThreshold: number;
    }

    /**
     * 
     */
    export interface IAxisSchema {
        axis: string;
        joystick: number;
        name: string;
    }

    export interface IDeviceLayerSettings {
        InputWriter: InputWritr.IInputWritr;
        triggers: ITriggers;
        aliases: IAliases;
    }

    export interface IDeviceLayr {
        getInputWritr(): InputWritr.IInputWritr;
        getTriggers(): ITriggers;
        getAliases(): IAliases;
        getGamepads(): IGamepad[];
        checkNavigatorGamepads(): number;
        registerGamepad(gamepad: IGamepad): void;
        activateAllGamepadTriggers(): void;
        activateGamepadTriggers(gamepad: IGamepad): void;
        activateAxisTrigger(gamepad: IGamepad, name: string, axis: string, magnitude: number): boolean;
        activateButtonTrigger(gamepad: IGamepad, name: string, status: boolean);
        clearAllGamepadTriggers(): void;
        clearGamepadTriggers(gamepad: IGamepad): void;
        clearAxisTrigger(gamepad: IGamepad, name: string, axis: string): void;
        clearButtonTrigger(gamepad: IGamepad, name: string): void;
    }
}


module DeviceLayr {
    "use strict";

    /**
     * Status possibilities for an axis. Neutral is the default; positive is above
     * its threshold; negative is below the threshold / -1.
     */
    export enum AxisStatus {
        /**
         * Low axis status (lower than the negative of the threshold).
         */
        negative,

        /**
         * Default axis status (absolutely closer to 0 than the threshold).
         */
        neutral,

        /**
         * High axis status (higher than the threshold).
         */
        positive
    }

    /**
     * 
     */
    export class DeviceLayr implements IDeviceLayr {
        /**
         * Known mapping schemas for standard controllers. These are referenced
         * by added gamepads via the gamepads' .name attribute.
         */
        private static controllerMappings: IControllerMappings = {
            /**
             * Controller mapping for a typical Xbox style controller.
             */
            "standard": {
                "axes": [
                    {
                        "axis": "x",
                        "joystick": 0,
                        "name": "leftJoystick"
                    },
                    {
                        "axis": "y",
                        "joystick": 0,
                        "name": "leftJoystick"
                    },
                    {
                        "axis": "x",
                        "joystick": 1,
                        "name": "rightJoystick"
                    },
                    {
                        "axis": "y",
                        "joystick": 1,
                        "name": "rightJoystick"
                    }
                ],
                "buttons": [
                    "a",
                    "b",
                    "x",
                    "y",
                    "leftTop",
                    "rightTop",
                    "leftTrigger",
                    "rightTrigger",
                    "select",
                    "start",
                    "leftStick",
                    "rightStick",
                    "dpadUp",
                    "dpadDown",
                    "dpadLeft",
                    "dpadRight"
                ],
                "joystickThreshold": .49
            }
        };

        /**
         * Internal InputWritr button and joystick triggers are piped to.
         */
        private InputWritr: InputWritr.IInputWritr;

        /**
         * Mapping of which device controls should cause what triggers, along
         * with their current statuses.
         */
        private triggers: ITriggers;

        /**
         * For "on" and "off" activations, the equivalent event keys to pass to
         * the internal InputWritr.
         */
        private aliases: IAliases;

        /**
         * Any added gamepads (devices), in order of activation.
         */
        private gamepads: IGamepad[];

        /**
         * @param {IDeviceLayerSettings} settings
         */
        constructor(settings: IDeviceLayerSettings) {
            this.InputWritr = settings.InputWriter;
            this.triggers = settings.triggers;
            this.aliases = settings.aliases;

            this.gamepads = [];
        }


        /* Simple gets
        */

        /**
         * @return {InputWritr} The internal InputWritr button and joystick triggers 
         *                      are piped to.
         */
        getInputWritr(): InputWritr.IInputWritr {
            return this.InputWritr;
        }

        /**
         * @return {Object} Mapping of which device controls should cause what triggers,
         *                  along with their current statuses.
         */
        getTriggers(): ITriggers {
            return this.triggers;
        }

        /**
         * @return {Object} For "on" and "off" activations, the equivalent event keys
         *                  to pass to the internal InputWritr.
         */
        getAliases(): IAliases {
            return this.aliases;
        }

        /**
         * @return {Gamepad[]} Any added gamepads (devices), in order of activation.
         */
        getGamepads(): IGamepad[] {
            return this.gamepads;
        }


        /* Registration
        */

        /**
         * If possible, checks the navigator for new gamepads, and adds them if found.
         * 
         * @return {Number} How many gamepads were added.
         */
        checkNavigatorGamepads(): number {
            if (typeof (<any>navigator).getGamepads === "undefined" || !(<any>navigator).getGamepads()[this.gamepads.length]) {
                return 0;
            }

            this.registerGamepad((<any>navigator).getGamepads()[this.gamepads.length]);

            return this.checkNavigatorGamepads() + 1;
        }

        /**
         * Registers a new gamepad.
         * 
         * @param {Gamepad} gamepad
         */
        registerGamepad(gamepad: IGamepad): void {
            this.gamepads.push(gamepad);
            this.setDefaultTriggerStatuses(gamepad, this.triggers);
        }


        /* Triggers
        */

        /**
         * Checks the trigger statuses of all known gamepads.
         */
        activateAllGamepadTriggers(): void {
            for (var i: number = 0; i < this.gamepads.length; i += 1) {
                this.activateGamepadTriggers(this.gamepads[i]);
            }
        }

        /**
         * Checks the trigger status of a gamepad, calling the equivalent InputWritr
         * events if any triggers have occurred.
         * 
         * @param {Gamepad} gamepad
         */
        activateGamepadTriggers(gamepad: IGamepad): void {
            var mapping: IControllerMapping = DeviceLayr.controllerMappings[gamepad.mapping || "standard"],
                i: number;

            for (i = 0; i < mapping.axes.length; i += 1) {
                this.activateAxisTrigger(gamepad, mapping.axes[i].name, mapping.axes[i].axis, gamepad.axes[i]);
            }

            for (i = 0; i < mapping.buttons.length; i += 1) {
                this.activateButtonTrigger(gamepad, mapping.buttons[i], gamepad.buttons[i].pressed);
            }
        }

        /**
         * Checks for triggered changes to an axis, and calls the equivalent InputWritr
         * event if one is found.
         * 
         * @param {Gamepad} gamepad
         * @param {String} name   The name of the axis, typically "x" or "y".
         * @param {Number} magnitude   The current value of the axis, in [1, -1].
         * @return {Boolean} Whether the trigger was activated.
         */
        activateAxisTrigger(gamepad: IGamepad, name: string, axis: string, magnitude: number): boolean {
            var listing: IJoystickTriggerAxis = (<IJoystickListing>this.triggers[name])[axis],
                status: AxisStatus;

            if (!listing) {
                return;
            }

            // If the axis' current status matches the new one, don't do anything
            status = this.getAxisStatus(gamepad, magnitude);
            if (listing.status === status) {
                return false;
            }

            // If it exists, release the old axis via the InputWritr using the off alias
            if (listing.status !== undefined && listing[AxisStatus[listing.status]] !== undefined) {
                this.InputWritr.callEvent(this.aliases.off, listing[AxisStatus[listing.status]]);
            }

            // Mark the new status in the listing
            listing.status = status;

            // Trigger the new status via the InputWritr using the on alias
            if (listing[AxisStatus[status]] !== undefined) {
                this.InputWritr.callEvent(this.aliases.on, listing[AxisStatus[status]]);
            }

            return true;
        }

        /**
         * Checks for triggered changes to a button, and calls the equivalent InputWritr
         * event if one is found.
         * 
         * @param {Gamepad} gamepad
         * @param {String} name   The name of the button, such as "a" or "left".
         * @param {Boolean} status   Whether the button is activated (pressed).
         * @return {Boolean} Whether the trigger was activated.
         */
        activateButtonTrigger(gamepad: IGamepad, name: string, status: boolean): boolean {
            var listing: IButtonListing = <IButtonListing>this.triggers[name];

            // If the button's current status matches the new one, don't do anything
            if (!listing || listing.status === status) {
                return false;
            }

            listing.status = status;

            // Trigger the new status via the InputWritr using the new alias
            this.InputWritr.callEvent(status ? this.aliases.on : this.aliases.off, listing.trigger);

            return true;
        }

        /**
         * Clears the statuses of all axes and buttons on all known gamepads.
         */
        clearAllGamepadTriggers(): void {
            for (var i: number = 0; i < this.gamepads.length; i += 1) {
                this.clearGamepadTriggers(this.gamepads[i]);
            }
        }

        /**
         * Clears the status of all axes and buttons on a gamepad.
         * 
         * @param {Gamepad} gamepad
         */
        clearGamepadTriggers(gamepad: IGamepad): void {
            var mapping: IControllerMapping = DeviceLayr.controllerMappings[gamepad.mapping || "standard"],
                i: number;

            for (i = 0; i < mapping.axes.length; i += 1) {
                this.clearAxisTrigger(gamepad, mapping.axes[i].name, mapping.axes[i].axis);
            }

            for (i = 0; i < mapping.buttons.length; i += 1) {
                this.clearButtonTrigger(gamepad, mapping.buttons[i]);
            }
        }

        /**
         * Sets the status of an axis to neutral.
         * 
         * @param {Gamepad} gamepad
         * @param {String} name   The name of the axis, typically "x" or "y".
         */
        clearAxisTrigger(gamepad: IGamepad, name: string, axis: string): void {
            var listing: IJoystickTriggerAxis = (<IJoystickListing>this.triggers[name])[axis];

            listing.status = AxisStatus.neutral;
        }

        /**
         * Sets the status of a button to off.
         * 
         * @param {Gamepad} gamepad
         * @param {String} name   The name of the button, such as "a" or "left".
         */
        clearButtonTrigger(gamepad: IGamepad, name: string): void {
            var listing: IButtonListing = <IButtonListing>this.triggers[name];

            listing.status = false;
        }


        /* Private utilities
        */

        /**
         * Puts the default values for all buttons and joystick axes that don't already
         * have statuses. This is useful so activation checks don't glitch out.
         * 
         * @param {Gamepad} gamepad
         * @param {Object} [triggers]
         */
        private setDefaultTriggerStatuses(gamepad: IGamepad, triggers: ITriggers = this.triggers): void {
            var mapping: IControllerMapping = DeviceLayr.controllerMappings[gamepad.mapping || "standard"],
                button: IButtonListing,
                joystick: IJoystickListing,
                i: number,
                j: string;

            for (i = 0; i < mapping.buttons.length; i += 1) {
                button = <IButtonListing>triggers[mapping.buttons[i]];

                if (button && button.status === undefined) {
                    button.status = false;
                }
            }

            for (i = 0; i < mapping.axes.length; i += 1) {
                joystick = <IJoystickListing>triggers[mapping.axes[i].name];

                for (j in joystick) {
                    if (!joystick.hasOwnProperty(j)) {
                        continue;
                    }

                    if (joystick[j].status === undefined) {
                        joystick[j].status = AxisStatus.neutral;
                    }
                }
            }
        }

        /**
         * @param {Gamepad} gamepad
         * @param {Number} magnitude   The direction an axis is measured at, in [-1, 1].
         * @return {AxisStatus} What direction a magnitude is relative to 0 (namely
         *                      positive, negative, or neutral).
         */
        private getAxisStatus(gamepad: IGamepad, magnitude: number): AxisStatus {
            var joystickThreshold: number = DeviceLayr.controllerMappings[gamepad.mapping || "standard"].joystickThreshold;

            if (magnitude > joystickThreshold) {
                return AxisStatus.positive;
            }

            if (magnitude < -joystickThreshold) {
                return AxisStatus.negative;
            }

            return AxisStatus.neutral;
        }
    }
}
