/// <reference path="ChangeLinr-0.2.0.ts" />
/// <reference path="ObjectMakr-0.2.2.ts" />
/// <reference path="PixelRendr-0.2.0.ts" />
/// <reference path="QuadsKeepr-0.2.1.ts" />
/// <reference path="StringFilr-0.2.1.ts" />

declare module PixelDrawr {
    /**
     * A typed array of 8-bit unsigned integer values. The contents are initialized 
     * to 0. If the requested number of bytes could not be allocated an exception is
     * raised.
     */
    interface Uint8ClampedArray extends ArrayBufferView {
        [index: number]: number;

        /**
          * The size in bytes of each element in the array. 
          */
        BYTES_PER_ELEMENT: number;

        /**
          * The length of the array.
          */
        length: number;

        /**
          * Gets the element at the specified index.
          * 
          * @param {Number} index The index at which to get the element of the array.
          */
        get(index: number): number;

        /**
          * Sets a value or an array of values.
          * 
          * @param {Number} index   The index of the location to set.
          * @param {Number} value   The value to set.
          */
        set(index: number, value: number): void;

        /**
          * Sets a value or an array of values.
          * 
          * @param {Uint8ClampedArray} array   A typed or untyped array of values 
          *                                    to set.
          * @param {Number} [offset]   The index in the current array at which the 
          *                            values are to be written.
          */
        set(array: Uint8ClampedArray, offset?: number): void;

        /**
          * Sets a value or an array of values.
          * 
          * @param {Number[]} array   A typed or untyped array of values to set.
          * @param {Number} [offset]   The index in the current array at which the 
          *                            values are to be written.
          */
        set(array: number[], offset?: number): void;

        /**
          * Gets a new Uint8ClampedArray view of the ArrayBuffer Object store for 
          * this array, specifying the first and last members of the subarray. 
          * 
          * @param {Number} begin   The index of the beginning of the array.
          * @param {Number} end   The index of the end of the array.
          */
        subarray(begin: number, end?: number): Uint8ClampedArray;
    }

    var Uint8ClampedArray: {
        prototype: Uint8ClampedArray;
        new (length: number): Uint8ClampedArray;
        new (array: Uint8ClampedArray): Uint8ClampedArray;
        new (array: number[]): Uint8ClampedArray;
        new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Uint8ClampedArray;
        BYTES_PER_ELEMENT: number;
    }

    export interface IScreenBoundaries {
        top: number;
        right: number;
        bottom: number;
        left: number;
    }

    export interface IThingCanvases {
        direction: string;
        multiple: boolean;
        middle?: IThingSubCanvas;
        top?: IThingSubCanvas;
        right?: IThingSubCanvas;
        bottom?: IThingSubCanvas;
        left?: IThingSubCanvas;
        topRight?: IThingSubCanvas;
        bottomRight?: IThingSubCanvas;
        bottomLeft?: IThingSubCanvas;
        topLeft?: IThingSubCanvas;
    }

    export interface IThingSubCanvas {
        "canvas": HTMLCanvasElement;
        "context": CanvasRenderingContext2D;
    }

    export interface IThing {
        /**
         * The sprite for this Thing to have drawn.
         */
        sprite: Uint8ClampedArray | PixelRendr.ISpriteMultiple;

        /**
         * The canvas upon which the Thing's sprite is to be drawn.
         */
        canvas: HTMLCanvasElement;

        /**
         * For Things with multiple sprites, the various sprite component canvases.
         */
        canvases?: IThingCanvases;

        /**
         * The rendering context used to draw the Thing's sprite on its canvas.
         */
        context: CanvasRenderingContext2D;

        /**
         * Whether this shouldn't be drawn (is completely hidden).
         */
        hidden: boolean;

        /**
         * How transparent this is, in [0, 1].
         */
        opacity: number;

        /**
         * How many sprites this has (1 for regular, 0 or >1 for multiple).
         */
        numSprites?: number;

        /**
         * Whether the Thing's sprite should repeat across large canvases.
         */
        repeat?: boolean;

        /**
         * How much to expand the Thing's sprite size (by default, 1 for not at all).
         */
        scale?: number;

        /**
         * Width in game pixels, equal to width * unitsize.
         */
        unitwidth?: number;

        /**
         * Height in game pixels, equal to height * unitsize.
         */
        unitheight?: number;

        /**
         * How many pixels wide the output sprite should be.
         */
        spritewidth: number;

        /**
         * How many pixels high the output sprite should be.
         */
        spriteheight: number;

        /**
         * Sprite width in real-life pixels, equal to spritewidth * scale.
         */
        spritewidthpixels?: number;

        /**
         * Sprite height in real-life pixels, equal to spritewidth * scale.
         */
        spriteheightpixels?: number;
    }

    export interface IPixelDrawrSettings {
        /**
         * The PixelRendr used for sprite lookups and generation.
         */
        PixelRender: PixelRendr.IPixelRendr;

        /**
         * The bounds of the screen for bounds checking (typically a MapScreenr).
         */
        MapScreener: IScreenBoundaries;

        /**
         * A Function to create a canvas of a given width and height.
         */
        createCanvas: (width: number, height: number) => HTMLCanvasElement;

        /**
         * How much to scale canvases on creation (by default, 1 for not at all).
         */
        unitsize?: number;

        /**
         * Whether refills should skip redrawing the background each time.
         */
        noRefill?: boolean;

        /**
         * The maximum size of a SpriteMultiple to pre-render (by default, 0 for
         * never pre-rendering).
         */
        spriteCacheCutoff?: number;

        /**
         * The names of groups to refill (only used if using Quadrant refilling).
         */
        groupNames?: string[];

        /**
         * How often to draw frames (by default, 1 for every time).
         */
        framerateSkip?: number;

        /**
         * How to generat ekeys to retrieve sprites from the PixelRendr (by default,
         * Object.toString).
         */
        generateObjectKey?: (thing: IThing) => string;

        /**
         * An arbitrarily small minimum opacity for a Thing to be considered not
         * completely transparent (by default, .007).
         */
        epsilon?: number;

        /**
         * The attribute name for a Thing's width (by default, "width").
         */
        keyWidth?: string;

        /**
         * The attribute name for a Thing's height (by default, "height").
         */
        keyHeight?: string;

        /**
         * The attribute name for a Thing's top (by default, "top").
         */
        keyTop?: string;

        /**
         * The attribute name for a Thing's right (by default, "right").
         */
        keyRight?: string;

        /**
         * The attribute name for a Thing's bottom (by default, "bottom").
         */
        keyBottom?: string;

        /**
         * The attribute name for a Thing's left (by default, "left").
         */
        keyLeft?: string;

        /**
         * The attribute name for a Thing's horizontal offest (by default, ignored).
         */
        keyOffsetX?: string;

        /**
         * The attribute name for a Thing's vertical offset(by default, ignored).
         */
        keyOffsetY?: string;
    }

    export interface IPixelDrawr {
        getFramerateSkip(): number;
        getThingArray(): IThing[][];
        getCanvas(): HTMLCanvasElement;
        getContext(): CanvasRenderingContext2D;
        getBackgroundCanvas(): HTMLCanvasElement;
        getBackgroundContext(): CanvasRenderingContext2D;
        getNoRefill(): boolean;
        getEpsilon(): number;
        setFramerateSkip(framerateSkip: number): void;
        setThingArrays(thingArrays: IThing[][]): void;
        setCanvas(canvas: HTMLCanvasElement): void;
        setNoRefill(noRefill: boolean): void;
        setEpsilon(epsilon: number): void;
        resetBackground(): void;
        setBackground(fillStyle: any): void;
        drawBackground(): void;
        setThingSprite(thing: IThing): void;
        refillThingCanvasSingle(thing: IThing): void;
        refillThingCanvasMultiple(thing: IThing): void;
        refillGlobalCanvas(): void;
        refillThingArray(array: IThing[]): void;
        refillQuadrantGroups(groups: QuadsKeepr.IQuadrantRow[]): void;
        refillQuadrants(quadrants: QuadsKeepr.IQuadrant[]): void;
        refillQuadrant(quadrant: QuadsKeepr.IQuadrant): void;
        drawThingOnContext(context: CanvasRenderingContext2D, thing: IThing): void;
        drawThingOnQuadrant(thing: IThing, quadrant: QuadsKeepr.IQuadrant): void;
        drawThingOnContextSingle(
            context: CanvasRenderingContext2D,
            canvas: HTMLCanvasElement,
            thing: IThing,
            left: number,
            top: number): void;
        drawThingOnContextMultiple(
            context: CanvasRenderingContext2D,
            canvases: IThingCanvases,
            thing: IThing,
            left: number,
            top: number
            ): void;
    }
}


module PixelDrawr {
    "use strict";

    /**
     * A front-end to PixelRendr to automate drawing mass amounts of sprites to a
     * primary canvas. A PixelRendr keeps track of sprite sources, while a
     * MapScreenr maintains boundary information on the screen. Global screen 
     * refills may be done by drawing every Thing in the thingArrays, or by 
     * Quadrants as a form of dirty rectangles.
     */
    export class PixelDrawr implements IPixelDrawr {
        /**
         * A PixelRendr used to obtain raw sprite data and canvases.
         */
        private PixelRender: PixelRendr.IPixelRendr;

        /**
         * The bounds of the screen for bounds checking (often a MapScreenr).
         */
        private MapScreener: IScreenBoundaries;

        /**
         * The canvas element each Thing is to be drawn on.
         */
        private canvas: HTMLCanvasElement;

        /**
         * The 2D canvas context associated with the canvas.
         */
        private context: CanvasRenderingContext2D;

        /**
         * A separate canvas that keeps the background of the scene.
         */
        private backgroundCanvas: HTMLCanvasElement;

        /**
         * The 2D canvas context associated with the background canvas.
         */
        private backgroundContext: CanvasRenderingContext2D;

        /**
         * Arrays of Thing[]s that are to be drawn in each refill.
         */
        private thingArrays: IThing[][];

        /**
         * Utility Function to create a canvas.
         */
        private createCanvas: (width: number, height: number) => HTMLCanvasElement;

        /**
         * How much to scale canvases on creation.
         */
        private unitsize: number;

        /**
         * Utility Function to generate a class key for a Thing.
         */
        private generateObjectKey: (thing: IThing) => string;

        /**
         * The maximum size of a SpriteMultiple to pre-render.
         */
        private spriteCacheCutoff: number;

        /**
         * Whether refills should skip redrawing the background each time.
         */
        private noRefill: boolean;

        /**
         * For refillQuadrant, an Array of String names to refill (bottom-to-top).
         */
        private groupNames: string[];

        /**
         * How often the screen redraws (1 for always, 2 for every other call, etc).
         */
        private framerateSkip: number;

        /**
         * How many frames have been drawn so far.
         */
        private framesDrawn: number;

        /**
         * An arbitrarily small minimum for opacity to be completely transparent.
         */
        private epsilon: number;

        /**
         * String key under which Things store their height.
         */
        private keyHeight: string;

        /**
         * String key under which Things store their width.
         */
        private keyWidth: string;

        /**
         * String key under which Things store their top.
         */
        private keyTop: string;

        /**
         * String key under which Things store their right.
         */
        private keyRight: string;

        /**
         * String key under which Things store their bottom.
         */
        private keyBottom: string;

        /**
         * String key under which Things store their left.
         */
        private keyLeft: string;

        /**
         * String key under which Things store their horizontal offset.
         */
        private keyOffsetX: string;

        /**
         * String key under which Things store their vertical offset.
         */
        private keyOffsetY: string;

        /**
         * @param {IPixelDrawrSettings} settings
         */
        constructor(settings: IPixelDrawrSettings) {
            this.PixelRender = settings.PixelRender;
            this.MapScreener = settings.MapScreener;
            this.createCanvas = settings.createCanvas;

            this.unitsize = settings.unitsize || 1;
            this.noRefill = settings.noRefill;
            this.spriteCacheCutoff = settings.spriteCacheCutoff || 0;
            this.groupNames = settings.groupNames;
            this.framerateSkip = settings.framerateSkip || 1;
            this.framesDrawn = 0;
            this.epsilon = settings.epsilon || .007;

            this.keyWidth = settings.keyWidth || "width";
            this.keyHeight = settings.keyHeight || "height";
            this.keyTop = settings.keyTop || "top";
            this.keyRight = settings.keyRight || "right";
            this.keyBottom = settings.keyBottom || "bottom";
            this.keyLeft = settings.keyLeft || "left";
            this.keyOffsetX = settings.keyOffsetX;
            this.keyOffsetY = settings.keyOffsetY;

            this.generateObjectKey = settings.generateObjectKey || function (thing: IThing): string {
                return thing.toString();
            };

            this.resetBackground();
        }


        /* Simple gets
        */

        /**
         * @return {Number} How often refill calls should be skipped.
         */
        getFramerateSkip(): number {
            return this.framerateSkip;
        }

        /**
         * @return {Array[]} The Arrays to be redrawn during refill calls.
         */
        getThingArray(): IThing[][] {
            return this.thingArrays;
        }

        /**
         * @return {HTMLCanvasElement} The canvas element each Thing is to drawn on.
         */
        getCanvas(): HTMLCanvasElement {
            return this.canvas;
        }

        /**
         * @return {CanvasRenderingContext2D} The 2D canvas context associated with
         *                                    the canvas.
         */
        getContext(): CanvasRenderingContext2D {
            return this.context;
        }

        /**
         * @return {HTMLCanvasElement} The canvas element used for the background.
         */
        getBackgroundCanvas(): HTMLCanvasElement {
            return this.backgroundCanvas;
        }

        /**
         * @return {CanvasRenderingContext2D} The 2D canvas context associated with
         *                                    the background canvas.
         */
        getBackgroundContext(): CanvasRenderingContext2D {
            return this.backgroundContext;
        }

        /**
         * @return {Boolean} Whether refills should skip redrawing the background 
         *                   each time.
         */
        getNoRefill(): boolean {
            return this.noRefill;
        }

        /**
         * @return {Number} The minimum opacity that will be drawn.
         */
        getEpsilon(): number {
            return this.epsilon;
        }


        /* Simple sets
        */

        /**
         * @param {Number} framerateSkip   How often refill calls should be skipped.
         */
        setFramerateSkip(framerateSkip: number): void {
            this.framerateSkip = framerateSkip;
        }

        /**
         * @param {Array[]} thingArrays   The Arrays to be redrawn during refill calls.
         */
        setThingArrays(thingArrays: IThing[][]): void {
            this.thingArrays = thingArrays;
        }

        /**
         * Sets the currently drawn canvas and context, and recreates 
         * drawThingOnContextBound.
         * 
         * @param {HTMLCanvasElement} canvas   The new primary canvas to be used.
         */
        setCanvas(canvas: HTMLCanvasElement): void {
            this.canvas = canvas;
            this.context = <CanvasRenderingContext2D>canvas.getContext("2d");
        }

        /**
         * @param {Boolean} noRefill   Whether refills should now skip redrawing the 
         *                             background each time. 
         */
        setNoRefill(noRefill: boolean): void {
            this.noRefill = noRefill;
        }

        /**
         * @param {Number} The minimum opacity that will be drawn.
         */
        setEpsilon(epsilon: number): void {
            this.epsilon = epsilon;
        }


        /* Background manipulations
        */

        /**
         * Creates a new canvas the size of MapScreener and sets the background
         * canvas to it, then recreates backgroundContext.
         */
        resetBackground(): void {
            this.backgroundCanvas = this.createCanvas(this.MapScreener[this.keyWidth], this.MapScreener[this.keyHeight]);
            this.backgroundContext = <CanvasRenderingContext2D>this.backgroundCanvas.getContext("2d");
        }

        /**
         * Refills the background canvas with a new fillStyle.
         * 
         * @param {Mixed} fillStyle   The new fillStyle for the background context.
         */
        setBackground(fillStyle: any): void {
            this.backgroundContext.fillStyle = fillStyle;
            this.backgroundContext.fillRect(0, 0, this.MapScreener[this.keyWidth], this.MapScreener[this.keyHeight]);
        }

        /**
         * Draws the background canvas onto the main canvas' context.
         */
        drawBackground(): void {
            this.context.drawImage(this.backgroundCanvas, 0, 0);
        }


        /* Core rendering
        */

        /**
         * Goes through all the motions of finding and parsing a Thing's sprite.
         * This should be called whenever the sprite's appearance changes.
         * 
         * @param {Thing} thing   A Thing whose sprite must be updated.
         * @return {Self}
         */
        setThingSprite(thing: IThing): void {
            // If it's set as hidden, don't bother updating it
            if (thing.hidden) {
                return;
            }

            // PixelRender does most of the work in fetching the rendered sprite
            thing.sprite = this.PixelRender.decode(this.generateObjectKey(thing), thing);

            // To do: remove dependency on .numSprites
            // For now, it's used to know whether it's had its sprite set, but 
            // wouldn't physically having a .sprite do that?
            if (thing.sprite.constructor === PixelRendr.SpriteMultiple) {
                thing.numSprites = 0;
                this.refillThingCanvasMultiple(thing);
            } else {
                thing.numSprites = 1;
                this.refillThingCanvasSingle(thing);
            }
        }

        /**
         * Simply draws a thing's sprite to its canvas by getting and setting
         * a canvas::imageData object via context.getImageData(...).
         * 
         * @param {Thing} thing   A Thing whose canvas must be updated.
         */
        refillThingCanvasSingle(thing: IThing): void {
            // Don't draw small Things.
            if (<any>thing[this.keyWidth] < 1 || <any>thing[this.keyHeight] < 1) {
                return;
            }

            // Retrieve the imageData from the Thing's canvas & renderingContext
            var canvas: HTMLCanvasElement = thing.canvas,
                context: CanvasRenderingContext2D = thing.context,
                imageData: ImageData = context.getImageData(0, 0, canvas[this.keyWidth], canvas[this.keyHeight]);

            // Copy the thing's sprite to that imageData and into the contextz
            this.PixelRender.memcpyU8(<Uint8ClampedArray>thing.sprite, imageData.data);
            context.putImageData(imageData, 0, 0);
        }

        /**
         * For SpriteMultiples, this copies the sprite information for each 
         * sub-sprite into its own canvas, sets thing.sprites, then draws the newly
         * rendered information onto the thing's canvas.
         * 
         * @param {Thing} thing   A Thing whose canvas and sprites must be updated.
         */
        refillThingCanvasMultiple(thing: IThing): void {
            if (thing[this.keyWidth] < 1 || thing[this.keyHeight] < 1) {
                return;
            }

            var spritesRaw: PixelRendr.SpriteMultiple = <PixelRendr.SpriteMultiple>thing.sprite,
                canvases: any = thing.canvases = {
                    "direction": spritesRaw.direction,
                    "multiple": true
                },
                canvas: HTMLCanvasElement,
                context: CanvasRenderingContext2D,
                imageData: ImageData,
                i: string;

            thing.numSprites = 1;

            for (i in spritesRaw.sprites) {
                if (!spritesRaw.sprites.hasOwnProperty(i)) {
                    continue;
                }

                // Make a new sprite for this individual component
                canvas = this.createCanvas(thing.spritewidth * this.unitsize, thing.spriteheight * this.unitsize);
                context = <CanvasRenderingContext2D>canvas.getContext("2d");

                // Copy over this sprite's information the same way as refillThingCanvas
                imageData = context.getImageData(0, 0, canvas[this.keyWidth], canvas[this.keyHeight]);
                this.PixelRender.memcpyU8(spritesRaw.sprites[i], imageData.data);
                context.putImageData(imageData, 0, 0);

                // Record the canvas and context in thing.sprites
                canvases[i] = {
                    "canvas": canvas,
                    "context": context
                };
                thing.numSprites += 1;
            }

            // Only pre-render multiple sprites if they're below the cutoff
            if (thing[this.keyWidth] * thing[this.keyHeight] < this.spriteCacheCutoff) {
                thing.canvas[this.keyWidth] = thing[this.keyWidth] * this.unitsize;
                thing.canvas[this.keyHeight] = thing[this.keyHeight] * this.unitsize;
                this.drawThingOnContextMultiple(thing.context, thing.canvases, thing, 0, 0);
            } else {
                thing.canvas[this.keyWidth] = thing.canvas[this.keyHeight] = 0;
            }
        }


        /* Core drawing
        */

        /**
         * Called every upkeep to refill the entire main canvas. All Thing arrays
         * are made to call this.refillThingArray in order.
         */
        refillGlobalCanvas(): void {
            this.framesDrawn += 1;
            if (this.framesDrawn % this.framerateSkip !== 0) {
                return;
            }

            if (!this.noRefill) {
                this.drawBackground();
            }

            for (var i: number = 0; i < this.thingArrays.length; i += 1) {
                this.refillThingArray(this.thingArrays[i]);
            }
        }

        /**
         * Calls drawThingOnContext on each Thing in the Array.
         * 
         * @param {Thing[]} array   A listing of Things to be drawn onto the canvas.
         */
        refillThingArray(array: IThing[]): void {
            for (var i: number = 0; i < array.length; i += 1) {
                this.drawThingOnContext(this.context, array[i]);
            }
        }

        /**
         * Refills the main canvas by calling refillQuadrants on each Quadrant in
         * the groups.
         * 
         * @param {QuadrantRow[]} groups   QuadrantRows (or QuadrantCols) to be
         *                                 redrawn to the canvas.
         */
        refillQuadrantGroups(groups: QuadsKeepr.IQuadrantRow[]): void {
            var i: number;

            this.framesDrawn += 1;
            if (this.framesDrawn % this.framerateSkip !== 0) {
                return;
            }

            for (i = 0; i < groups.length; i += 1) {
                this.refillQuadrants(groups[i].quadrants);
            }
        }

        /**
         * Refills (part of) the main canvas by drawing each Quadrant's canvas onto 
         * it.
         * 
         * @param {Quadrant[]} quadrants   The Quadrants to have their canvases 
         *                                 refilled.
         */
        refillQuadrants(quadrants: QuadsKeepr.IQuadrant[]): void {
            var quadrant: QuadsKeepr.IQuadrant,
                i: number;

            for (i = 0; i < quadrants.length; i += 1) {
                quadrant = quadrants[i];
                if (
                    quadrant.changed
                    && quadrant[this.keyTop] < this.MapScreener[this.keyHeight]
                    && quadrant[this.keyRight] > 0
                    && quadrant[this.keyBottom] > 0
                    && quadrant[this.keyLeft] < this.MapScreener[this.keyWidth]) {
                    this.refillQuadrant(quadrant);
                    this.context.drawImage(quadrant.canvas, quadrant[this.keyLeft], quadrant[this.keyTop]);
                }
            }
        }

        /**
         * Refills a Quadrants's canvas by resetting its background and drawing all
         * its Things onto it.
         * 
         * @param {Quadrant} quadrant   A quadrant whose Things must be drawn onto
         *                              its canvas.
         */
        refillQuadrant(quadrant: QuadsKeepr.IQuadrant): void {
            var group: IThing[],
                i: number,
                j: number;

            // This may be what's causing such bad performance.
            if (!this.noRefill) {
                quadrant.context.drawImage(
                    this.backgroundCanvas,
                    quadrant[this.keyLeft],
                    quadrant[this.keyTop],
                    quadrant.canvas[this.keyWidth],
                    quadrant.canvas[this.keyHeight],
                    0,
                    0,
                    quadrant.canvas[this.keyWidth],
                    quadrant.canvas[this.keyHeight]);
            }

            for (i = this.groupNames.length - 1; i >= 0; i -= 1) {
                group = quadrant.things[this.groupNames[i]];

                for (j = 0; j < group.length; j += 1) {
                    this.drawThingOnQuadrant(group[j], quadrant);
                }
            }

            quadrant.changed = false;
        }

        /**
         * General Function to draw a Thing onto a context. This will call
         * drawThingOnContext[Single/Multiple] with more arguments
         * 
         * @param {CanvasRenderingContext2D} context   The context to have the Thing
         *                                             drawn on it.
         * @param {Thing} thing   The Thing to be drawn onto the context.
         */
        drawThingOnContext(context: CanvasRenderingContext2D, thing: IThing): void {
            if (
                thing.hidden
                || thing.opacity < this.epsilon
                || thing[this.keyHeight] < 1
                || thing[this.keyWidth] < 1
                || this.getTop(thing) > this.MapScreener[this.keyHeight]
                || this.getRight(thing) < 0
                || this.getBottom(thing) < 0
                || this.getLeft(thing) > this.MapScreener[this.keyWidth]) {
                return;
            }

            // If Thing hasn't had a sprite yet (previously hidden), do that first
            if (typeof thing.numSprites === "undefined") {
                this.setThingSprite(thing);
            }

            // Whether or not the thing has a regular sprite or a SpriteMultiple, 
            // that sprite has already been drawn to the thing's canvas, unless it's
            // above the cutoff, in which case that logic happens now.
            if (thing.canvas[this.keyWidth] > 0) {
                this.drawThingOnContextSingle(context, thing.canvas, thing, this.getLeft(thing), this.getTop(thing));
            } else {
                this.drawThingOnContextMultiple(context, thing.canvases, thing, this.getLeft(thing), this.getTop(thing));
            }
        }

        /**
         * Draws a Thing onto a quadrant's canvas. This is a simple wrapper around
         * drawThingOnContextSingle/Multiple that also bounds checks.
         * 
         * @param {Thing} thing
         * @param {Quadrant} quadrant
         */
        drawThingOnQuadrant(thing: IThing, quadrant: QuadsKeepr.IQuadrant): void {
            if (
                thing.hidden
                || this.getTop(thing) > quadrant[this.keyBottom]
                || this.getRight(thing) < quadrant[this.keyLeft]
                || this.getBottom(thing) < quadrant[this.keyTop]
                || this.getLeft(thing) > quadrant[this.keyRight]
                || thing.opacity < this.epsilon) {
                return;
            }

            // If there's just one sprite, it's pretty simple
            if (thing.numSprites === 1) {
                return this.drawThingOnContextSingle(
                    quadrant.context,
                    thing.canvas,
                    thing,
                    this.getLeft(thing) - quadrant[this.keyLeft],
                    this.getTop(thing) - quadrant[this.keyTop]);
            } else {
                // For multiple sprites, some calculations will be needed
                return this.drawThingOnContextMultiple(
                    quadrant.context,
                    thing.canvases,
                    thing,
                    this.getLeft(thing) - quadrant[this.keyLeft],
                    this.getTop(thing) - quadrant[this.keyTop]);
            }
        }

        /**
         * Draws a Thing's single canvas onto a context, commonly called by
         * this.drawThingOnContext.
         * 
         * @param {CanvasRenderingContext2D} context    The context being drawn on.
         * @param {Canvas} canvas   The Thing's canvas being drawn onto the context.
         * @param {Thing} thing   The Thing whose canvas is being drawn.
         * @param {Number} left   The x-position to draw the Thing from.
         * @param {Number} top   The y-position to draw the Thing from.
         */
        drawThingOnContextSingle(
            context: CanvasRenderingContext2D,
            canvas: HTMLCanvasElement,
            thing: IThing,
            left: number,
            top: number): void {
            // If the sprite should repeat, use the pattern equivalent
            if (thing.repeat) {
                this.drawPatternOnContext(context, canvas, left, top, thing.unitwidth, thing.unitheight, thing.opacity || 1);
            } else if (thing.opacity !== 1) {
                // Opacities not equal to one must reset the context afterwards
                context.globalAlpha = thing.opacity;
                context.drawImage(canvas, left, top, canvas.width * thing.scale, canvas.height * thing.scale);
                context.globalAlpha = 1;
            } else {
                context.drawImage(canvas, left, top, canvas.width * thing.scale, canvas.height * thing.scale);
            }
        }

        /**
         * Draws a Thing's multiple canvases onto a context, typicall called by
         * drawThingOnContext. A variety of cases for canvases is allowed:
         * "vertical", "horizontal", and "corners".
         * 
         * @param {CanvasRenderingContext2D} context    The context being drawn on.
         * @param {Canvas} canvases   The canvases being drawn onto the context.
         * @param {Thing} thing   The Thing whose canvas is being drawn.
         * @param {Number} left   The x-position to draw the Thing from.
         * @param {Number} top   The y-position to draw the Thing from.
         */
        drawThingOnContextMultiple(
            context: CanvasRenderingContext2D,
            canvases: IThingCanvases,
            thing: IThing,
            left: number,
            top: number): void {
            var sprite: PixelRendr.SpriteMultiple = <PixelRendr.SpriteMultiple>thing.sprite,
                topreal: number = top,
                leftreal: number = left,
                rightreal: number = left + thing.unitwidth,
                bottomreal: number = top + thing.unitheight,
                widthreal: number = thing.unitwidth,
                heightreal: number = thing.unitheight,
                spritewidthpixels: number = thing.spritewidthpixels,
                spriteheightpixels: number = thing.spriteheightpixels,
                widthdrawn: number = Math.min(widthreal, spritewidthpixels),
                heightdrawn: number = Math.min(heightreal, spriteheightpixels),
                opacity: number = thing.opacity,
                diffhoriz: number,
                diffvert: number,
                canvasref: IThingSubCanvas;

            switch (canvases.direction) {
                // Vertical sprites may have 'top', 'bottom', 'middle'
                case "vertical":
                    // If there's a bottom, draw that and push up bottomreal
                    if ((canvasref = <IThingSubCanvas>canvases[this.keyBottom])) {
                        diffvert = sprite.bottomheight ? sprite.bottomheight * this.unitsize : spriteheightpixels;
                        this.drawPatternOnContext(
                            context,
                            canvasref.canvas,
                            leftreal,
                            bottomreal - diffvert,
                            widthreal,
                            heightdrawn,
                            opacity);
                        bottomreal -= diffvert;
                        heightreal -= diffvert;
                    }
                    // If there's a top, draw that and push down topreal
                    if ((canvasref = <IThingSubCanvas>canvases[this.keyTop])) {
                        diffvert = sprite.topheight ? sprite.topheight * this.unitsize : spriteheightpixels;
                        this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthreal, heightdrawn, opacity);
                        topreal += diffvert;
                        heightreal -= diffvert;
                    }
                    break;
                // Horizontal sprites may have 'left', 'right', 'middle'
                case "horizontal":
                    // If there's a left, draw that and push forward leftreal
                    if ((canvasref = canvases[this.keyLeft])) {
                        diffhoriz = sprite.leftwidth ? sprite.leftwidth * this.unitsize : spritewidthpixels;
                        this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthdrawn, heightreal, opacity);
                        leftreal += diffhoriz;
                        widthreal -= diffhoriz;
                    }
                    // If there's a right, draw that and push back rightreal
                    if ((canvasref = canvases[this.keyRight])) {
                        diffhoriz = sprite.rightwidth ? sprite.rightwidth * this.unitsize : spritewidthpixels;
                        this.drawPatternOnContext(
                            context,
                            canvasref.canvas,
                            rightreal - diffhoriz,
                            topreal,
                            widthdrawn,
                            heightreal,
                            opacity);
                        rightreal -= diffhoriz;
                        widthreal -= diffhoriz;
                    }
                    break;
                // Corner (vertical + horizontal + corner) sprites must have corners
                // in 'topRight', 'bottomRight', 'bottomLeft', and 'topLeft'.
                case "corners":
                    // topLeft, left, bottomLeft
                    diffvert = sprite.topheight ? sprite.topheight * this.unitsize : spriteheightpixels;
                    diffhoriz = sprite.leftwidth ? sprite.leftwidth * this.unitsize : spritewidthpixels;
                    this.drawPatternOnContext(
                        context,
                        canvases.topLeft.canvas,
                        leftreal,
                        topreal,
                        widthdrawn,
                        heightdrawn,
                        opacity);
                    this.drawPatternOnContext(
                        context,
                        canvases[this.keyLeft].canvas,
                        leftreal,
                        topreal + diffvert,
                        widthdrawn,
                        heightreal - diffvert * 2,
                        opacity);
                    this.drawPatternOnContext(
                        context,
                        canvases.bottomLeft.canvas,
                        leftreal,
                        bottomreal - diffvert,
                        widthdrawn,
                        heightdrawn,
                        opacity);
                    leftreal += diffhoriz;
                    widthreal -= diffhoriz;

                    // top, topRight
                    diffhoriz = sprite.rightwidth ? sprite.rightwidth * this.unitsize : spritewidthpixels;
                    this.drawPatternOnContext(
                        context,
                        canvases[this.keyTop].canvas,
                        leftreal,
                        topreal,
                        widthreal - diffhoriz,
                        heightdrawn,
                        opacity);
                    this.drawPatternOnContext(
                        context,
                        canvases.topRight.canvas,
                        rightreal - diffhoriz,
                        topreal,
                        widthdrawn,
                        heightdrawn,
                        opacity);
                    topreal += diffvert;
                    heightreal -= diffvert;

                    // right, bottomRight, bottom
                    diffvert = sprite.bottomheight ? sprite.bottomheight * this.unitsize : spriteheightpixels;
                    this.drawPatternOnContext(
                        context,
                        canvases[this.keyRight].canvas,
                        rightreal - diffhoriz,
                        topreal,
                        widthdrawn,
                        heightreal - diffvert,
                        opacity);
                    this.drawPatternOnContext(
                        context,
                        canvases.bottomRight.canvas,
                        rightreal - diffhoriz,
                        bottomreal - diffvert,
                        widthdrawn,
                        heightdrawn,
                        opacity);
                    this.drawPatternOnContext(
                        context,
                        canvases[this.keyBottom].canvas,
                        leftreal,
                        bottomreal - diffvert,
                        widthreal - diffhoriz,
                        heightdrawn,
                        opacity);
                    rightreal -= diffhoriz;
                    widthreal -= diffhoriz;
                    bottomreal -= diffvert;
                    heightreal -= diffvert;
                    break;
                default:
                    throw new Error("Unknown or missing direction given in SpriteMultiple.");
            }

            // If there's still room, draw the actual canvas
            if ((canvasref = canvases.middle) && topreal < bottomreal && leftreal < rightreal) {
                if (sprite.middleStretch) {
                    context.globalAlpha = opacity;
                    context.drawImage(canvasref.canvas, leftreal, topreal, widthreal, heightreal);
                    context.globalAlpha = 1;
                } else {
                    this.drawPatternOnContext(context, canvasref.canvas, leftreal, topreal, widthreal, heightreal, opacity);
                }
            }
        }


        /* Position utilities (which should almost always be optimized)
        */

        /**
         * @param {Thing} thing
         * @return {Number} The Thing's top position, accounting for vertical
         *                  offset if needed.
         */
        private getTop(thing: IThing): number {
            if (this.keyOffsetY) {
                return thing[this.keyTop] + thing[this.keyOffsetY];
            } else {
                return thing[this.keyTop];
            }
        }

        /**
         * @param {Thing} thing
         * @return {Number} The Thing's right position, accounting for horizontal 
         *                  offset if needed.
         */
        private getRight(thing: IThing): number {
            if (this.keyOffsetX) {
                return thing[this.keyRight] + thing[this.keyOffsetX];
            } else {
                return thing[this.keyRight];
            }
        }

        /**
         * @param {Thing} thing
         * @return {Number} The Thing's bottom position, accounting for vertical
         *                  offset if needed.
         */
        private getBottom(thing: IThing): number {
            if (this.keyOffsetX) {
                return thing[this.keyBottom] + thing[this.keyOffsetY];
            } else {
                return thing[this.keyBottom];
            }
        }

        /**
         * @param {Thing} thing
         * @return {Number} The Thing's left position, accounting for horizontal 
         *                  offset if needed.
         */
        private getLeft(thing: IThing): number {
            if (this.keyOffsetX) {
                return thing[this.keyLeft] + thing[this.keyOffsetX];
            } else {
                return thing[this.keyLeft];
            }
        }


        /* Utilities
        */

        /**
         * Draws a source pattern onto a context. The pattern is clipped to the size
         * of MapScreener.
         * 
         * @param {CanvasRenderingContext2D} context   The context the pattern will
         *                                             be drawn onto.
         * @param {Mixed} source   The image being repeated as a pattern. This can
         *                         be a canvas, an image, or similar.
         * @param {Number} left   The x-location to draw from.
         * @param {Number} top   The y-location to draw from.
         * @param {Number} width   How many pixels wide the drawing area should be.
         * @param {Number} left   How many pixels high the drawing area should be.
         * @param {Number} opacity   How transparent the drawing is, in [0,1].
         */
        private drawPatternOnContext(
            context: CanvasRenderingContext2D,
            source: any,
            left: number,
            top: number,
            width: number,
            height: number,
            opacity: number): void {
            context.globalAlpha = opacity;
            context.translate(left, top);
            context.fillStyle = context.createPattern(source, "repeat");
            context.fillRect(
                0, 0,
            // Math.max(width, left - MapScreener[keyRight]),
            // Math.max(height, top - MapScreener[keyBottom])
                width, height);
            context.translate(-left, -top);
            context.globalAlpha = 1;
        }
    }
}
