// js-imagediff 1.0.3
// (c) 2011-2012 Carl Sutherland, Humble Software
// Distributed under the MIT License
// For original source and documentation visit:
// http://www.github.com/HumbleSoftware/js-imagediff

(function (name, definition) {
    var root = this;
    if (typeof module !== 'undefined') {
        try {
            var Canvas = require('canvas');
        } catch (e) {
            throw new Error(
                e.message + '\n' +
                'Please see https://github.com/HumbleSoftware/js-imagediff#cannot-find-module-canvas\n'
            );
        }
        module.exports = definition(root, name, Canvas);
    } else if (typeof define === 'function' && typeof define.amd === 'object') {
        define(definition);
    } else {
        root[name] = definition(root, name);
    }
})('imagediff', function (root, name, Canvas) {

    var
    TYPE_ARRAY = /\[object Array\]/i,
        TYPE_CANVAS = /\[object (Canvas|HTMLCanvasElement)\]/i,
        TYPE_CONTEXT = /\[object CanvasRenderingContext2D\]/i,
        TYPE_IMAGE = /\[object (Image|HTMLImageElement)\]/i,
        TYPE_IMAGE_DATA = /\[object ImageData\]/i,

        UNDEFINED = 'undefined',

        canvas = getCanvas(),
        context = canvas.getContext('2d'),
        previous = root[name],
        imagediff, jasmine;

    // Creation
    function getCanvas(width, height) {
        var
        canvas = Canvas ?
            new Canvas() :
            document.createElement('canvas');
        if (width) canvas.width = width;
        if (height) canvas.height = height;
        return canvas;
    }

    function getImageData(width, height) {
        canvas.width = width;
        canvas.height = height;
        context.clearRect(0, 0, width, height);
        return context.createImageData(width, height);
    }


    // Type Checking
    function isImage(object) {
        return isType(object, TYPE_IMAGE);
    }

    function isCanvas(object) {
        return isType(object, TYPE_CANVAS);
    }

    function isContext(object) {
        return isType(object, TYPE_CONTEXT);
    }

    function isImageData(object) {
        return !!(object &&
            isType(object, TYPE_IMAGE_DATA) &&
            typeof (object.width) !== UNDEFINED &&
            typeof (object.height) !== UNDEFINED &&
            typeof (object.data) !== UNDEFINED);
    }

    function isImageType(object) {
        return (
            isImage(object) ||
            isCanvas(object) ||
            isContext(object) ||
            isImageData(object)
        );
    }

    function isType(object, type) {
        return typeof (object) === 'object' && !! Object.prototype.toString.apply(object).match(type);
    }


    // Type Conversion
    function copyImageData(imageData) {
        var
        height = imageData.height,
            width = imageData.width,
            data = imageData.data,
            newImageData, newData, i;

        canvas.width = width;
        canvas.height = height;
        newImageData = context.getImageData(0, 0, width, height);
        newData = newImageData.data;

        for (i = imageData.data.length; i--;) {
            newData[i] = data[i];
        }

        return newImageData;
    }

    function toImageData(object) {
        if (isImage(object)) {
            return toImageDataFromImage(object);
        }
        if (isCanvas(object)) {
            return toImageDataFromCanvas(object);
        }
        if (isContext(object)) {
            return toImageDataFromContext(object);
        }
        if (isImageData(object)) {
            return object;
        }
    }

    function toImageDataFromImage(image) {
        var
        height = image.height,
            width = image.width;
        canvas.width = width;
        canvas.height = height;
        context.clearRect(0, 0, width, height);
        context.drawImage(image, 0, 0);
        return context.getImageData(0, 0, width, height);
    }

    function toImageDataFromCanvas(canvas) {
        var
        height = canvas.height,
            width = canvas.width,
            context = canvas.getContext('2d');
        return context.getImageData(0, 0, width, height);
    }

    function toImageDataFromContext(context) {
        var
        canvas = context.canvas,
            height = canvas.height,
            width = canvas.width;
        return context.getImageData(0, 0, width, height);
    }

    function toCanvas(object) {
        var
        data = toImageData(object),
            canvas = getCanvas(data.width, data.height),
            context = canvas.getContext('2d');

        context.putImageData(data, 0, 0);
        return canvas;
    }


    // ImageData Equality Operators
    function equalWidth(a, b) {
        return a.width === b.width;
    }

    function equalHeight(a, b) {
        return a.height === b.height;
    }

    function equalDimensions(a, b) {
        return equalHeight(a, b) && equalWidth(a, b);
    }

    function equal(a, b, tolerance) {

        var
        aData = a.data,
            bData = b.data,
            length = aData.length,
            i;

        tolerance = tolerance || 0;

        if (!equalDimensions(a, b)) return false;
        e = true;
        for (i = length; i--;)
            if (aData[i] !== bData[i] && Math.abs(aData[i] - bData[i]) > tolerance) {
                console.log(i + "|" + aData[i] + "|" + bData[i]);
                e = false;
            }

        return e;
    }


    // Diff
    function diff(a, b, options) {
        return (equalDimensions(a, b) ? diffEqual : diffUnequal)(a, b, options);
    }

    function diffEqual(a, b, options) {

        var
        height = a.height,
            width = a.width,
            c = getImageData(width, height), // c = a - b
            aData = a.data,
            bData = b.data,
            cData = c.data,
            length = cData.length,
            row, column,
            i, j, k, v;

        for (i = 0; i < length; i += 4) {
            cData[i] = Math.abs(aData[i] - bData[i]);
            cData[i + 1] = Math.abs(aData[i + 1] - bData[i + 1]);
            cData[i + 2] = Math.abs(aData[i + 2] - bData[i + 2]);
            cData[i + 3] = Math.abs(255 - Math.abs(aData[i + 3] - bData[i + 3]));
        }

        return c;
    }

    function diffUnequal(a, b, options) {

        var
        height = Math.max(a.height, b.height),
            width = Math.max(a.width, b.width),
            c = getImageData(width, height), // c = a - b
            aData = a.data,
            bData = b.data,
            cData = c.data,
            align = options && options.align,
            rowOffset,
            columnOffset,
            row, column,
            i, j, k, v;


        for (i = cData.length - 1; i > 0; i = i - 4) {
            cData[i] = 255;
        }

        // Add First Image
        offsets(a);
        for (row = a.height; row--;) {
            for (column = a.width; column--;) {
                i = 4 * ((row + rowOffset) * width + (column + columnOffset));
                j = 4 * (row * a.width + column);
                cData[i + 0] = aData[j + 0]; // r
                cData[i + 1] = aData[j + 1]; // g
                cData[i + 2] = aData[j + 2]; // b
                // cData[i+3] = aData[j+3]; // a
            }
        }

        // Subtract Second Image
        offsets(b);
        for (row = b.height; row--;) {
            for (column = b.width; column--;) {
                i = 4 * ((row + rowOffset) * width + (column + columnOffset));
                j = 4 * (row * b.width + column);
                cData[i + 0] = Math.abs(cData[i + 0] - bData[j + 0]); // r
                cData[i + 1] = Math.abs(cData[i + 1] - bData[j + 1]); // g
                cData[i + 2] = Math.abs(cData[i + 2] - bData[j + 2]); // b
            }
        }

        // Helpers
        function offsets(imageData) {
            if (align === 'top') {
                rowOffset = 0;
                columnOffset = 0;
            } else {
                rowOffset = Math.floor((height - imageData.height) / 2);
                columnOffset = Math.floor((width - imageData.width) / 2);
            }
        }

        return c;
    }


    // Validation
    function checkType() {
        var i;
        for (i = 0; i < arguments.length; i++) {
            if (!isImageType(arguments[i])) {
                throw {
                    name: 'ImageTypeError',
                    message: 'Submitted object was not an image.'
                };
            }
        }
    }


    // Jasmine Matchers
    function get(element, content) {
        element = document.createElement(element);
        if (element && content) {
            element.innerHTML = content;
        }
        return element;
    }

    jasmine = {

			toBeImageData: function (util, customEqualityTesters) {
				return {
					compare: function (actual) {
					  return imagediff.isImageData(actual);
				  }
				}
			},

			toImageDiffEqual: function (util, customEqualityTesters) {
				return {
					compare: function (actual, expected, tolerance, maxDiffCount) {
						var message;
						if (typeof (document) !== UNDEFINED) {
							message = function () {
								var
									div = get('div'),
									a = get('div', '<div>Actual:</div>'),
									b = get('div', '<div>Expected:</div>'),
									c = get('div', '<div>Diff:</div>'),
									diff = imagediff.diff(actual, expected),
									canvas = getCanvas(),
									context;

								canvas.height = diff.height;
								canvas.width = diff.width;

								div.style.overflow = 'hidden';
								a.style.float = 'left';
								b.style.float = 'left';
								c.style.float = 'left';

								context = canvas.getContext('2d');
								context.putImageData(diff, 0, 0);

								a.appendChild(toCanvas(actual));
								b.appendChild(toCanvas(expected));
								c.appendChild(canvas);

								div.appendChild(a);
								div.appendChild(b);
								div.appendChild(c);

								return div;
							};
						}

						return { pass: imagediff.equal(actual, expected, tolerance, maxDiffCount), message: message () };
					}
				}
			}
    };


    // Image Output
    function imageDataToPNG(imageData, outputFile, callback) {

        var
         canvas = toCanvas(imageData),
					base64Data,
					decodedImage;

        callback = callback || Function;

        base64Data = canvas.toDataURL().replace(/^data:image\/\w+;base64,/, "");
        decodedImage = new Buffer(base64Data, 'base64');
        require('fs').writeFile(outputFile, decodedImage, callback);
    }


    // Definition
    imagediff = {

        createCanvas: getCanvas,
        createImageData: getImageData,

        isImage: isImage,
        isCanvas: isCanvas,
        isContext: isContext,
        isImageData: isImageData,
        isImageType: isImageType,

        toImageData: function (object) {
            checkType(object);
            if (isImageData(object)) {
                return copyImageData(object);
            }
            return toImageData(object);
        },

        equal: function (a, b, tolerance) {
            checkType(a, b);
            a = toImageData(a);
            b = toImageData(b);
            return equal(a, b, tolerance);
        },
        diff: function (a, b, options) {
            checkType(a, b);
            a = toImageData(a);
            b = toImageData(b);
            return diff(a, b, options);
        },

        jasmine: jasmine,

        // Compatibility
        noConflict: function () {
            root[name] = previous;
            return imagediff;
        }
    };

    if (typeof module !== 'undefined') {
        imagediff.imageDataToPNG = imageDataToPNG;
    }

    return imagediff;
});