'use strict';

Ox.load.Image = function(options, callback) {

    //@ Image ------------------------------------------------------------------

    /*@
    Ox.Image <f> Generic image object
        To render the image as an image element, use its <code>src()</code>
        method, to render it as a canvas, use its <code>canvas</code> property.
        (src, callback) -> <u> undefined
        (width, height, callback) -> <u> undefined
        (width, height, background, callback) -> <u> undefined
            src <s> Image source (local, remote or data URL)
            width <n> Width in px
            image <n> Height in px
            background <[n]> Background color (RGB or RGBA)
    @*/
    Ox.Image = function() {

        var self = {},
            that = {};

        self.callback = arguments[arguments.length - 1];
        if (arguments.length == 2) {
            self.src = arguments[0];
            self.image = new Image();
            self.image.onload = init;
            self.image.src = self.src;
        } else {
            self.width = arguments[0];
            self.height = arguments[1];
            self.background = arguments.length == 4
                ? arguments[2] : [0, 0, 0, 0];
            init();
        }

        function error(mode) {
            throw new RangeError('PNG codec can\'t ' + mode + ' ' + (
                mode == 'encode' ? 'data' : 'image'
            ));
        }

        function getCapacity(bpb) {
            var capacity = 0;
            that.forEach(function(rgba) {
                capacity += rgba[3] == 255 ? bpb * 3/8 : 0;
            });
            return capacity;
        }

        function getIndex() {
            if (arguments.length == 1) {
                xy = arguments[0];
            } else {
                xy = [arguments[0], arguments[1]];
            }
            return (
                Ox.mod(xy[0], self.width)
                + Ox.mod(xy[1] * self.width, self.width * self.height)
            ) * 4;
        }

        function getXY(index) {
            index /= 4;
            return [index % self.width, Math.floor(index / self.width)];
        }

        function init() {
            if (self.image) {
                self.width = self.image.width;
                self.height = self.image.height;
            }
            that.canvas = Ox.element('<canvas>').attr({
                width: self.width,
                height: self.height
            });
            that.context = that.canvas[0].getContext('2d');
            if (self.image) {
                that.context.drawImage(self.image, 0, 0);
            } else if (Ox.sum(self.background)) {
                that.context.fillStyle(self.background);
                that.context.fillRect(0, 0, self.width, self.height);
            }
            that.imageData = that.context.getImageData(
                0, 0, self.width, self.height
            );
            self.data = that.imageData.data;
            self.callback(that);
        }

        function setSL(sl, d) {
            var c = sl == 's' ? 1 : 2;
            return that.map(function(rgba) {
                var hsl = Ox.hsl([rgba[0], rgba[1], rgba[2]]), rgb;
                hsl[c] = d < 0 ? hsl[c] * (d + 1) : hsl[c] + (1 - hsl[c]) * d;
                rgb = Ox.rgb(hsl);
                return Ox.merge(rgb, rgba[3]);
            });
        }

        /*@
        blur <f> Apply blur filter
            (val) -> <o> The image object
            val <n> Amount of blur (1 to 5, more is slow)
        @*/
        that.blur = function(val) {
            var filter = [],
                size = val * 2 + 1;
                sum = 0
            Ox.loop(size, function(x) {
                Ox.loop(size, function(y) {
                    var isInCircle = +(Math.sqrt(
                        Math.pow(x - val, 2) + Math.pow(y - val, 2)
                    ) <= val);
                    sum += isInCircle;
                    filter.push(isInCircle)
                });
            });
            filter = filter.map(function(val) {
                return val / sum;
            })
            return that.filter(filter);
        };

        /*@
        channel <f> Reduce the image to one channel
            (channel) -> <o> The image object
            channel <str> 'r', 'g', 'b', 'a', 'h', 's' or 'l'
        @*/
        that.channel = function(str) {
            str = str[0].toLowerCase();
            return that.map(function(rgba) {
                var i = ['r', 'g', 'b', 'a'].indexOf(str), rgb, val;
                if (i > -1) {
                    return Ox.map(rgba, function(v, c) {
                        return str == 'a'
                            ? (c < 3 ? rgba[3] : 255)
                            : (c == i || c == 3 ? v : 0);
                    });
                } else {
                    i = ['h', 's', 'l'].indexOf(str);
                    val = Ox.hsl([rgba[0], rgba[1], rgba[2]])[i];
                    rgb = i == 0 
                        ? Ox.rgb([val, 1, 0.5])
                        : Ox.map(Ox.range(3), function(v) {
                            return parseInt(val * 255);
                        });
                    return Ox.merge(rgb, rgba[3]);
                }
            });
        }

        //@ context <o> 2D drawing context

        /*@
        contour <f> Apply contour filter
            () -> <o> The image object
        @*/
        that.contour = function(val) {
            return that.filter([
                +1, +1, +1,
                +1, -7, +1,
                +1, +1, +1
            ]);
        };

        /*@
        depth <f> Reduce the bit depth
            (depth) -> <o> The image object
            depth <n> Bits per channel (1 to 7)
        @*/
        that.depth = function(val) {
            var pow = Math.pow(2, 8 - val);
            return that.map(function(rgba) {
                return rgba.map(function(v, i) {
                    return i < 3 ? Math.floor(v / pow) * pow/* * 255 / val*/ : v;
                });
            });
        };

        /*@
        edges <f> Apply edges filter
            () -> <o> The image object
        @*/
        that.edges = function(val) {
            return that.filter([
                -1, -1, -1,
                -1, +8, -1,
                -1, -1, -1
            ]).saturation(-1);
        };

        /*@
        emboss <f> Apply emboss filter
            () -> <o> The image object
        @*/
        that.emboss = function(val) {
            return that.filter([
                -1, -1, +0,
                -1, +0, +1,
                +0, +1, +1
            ], 128).saturation(-1);
        };

        /*@
        encode <f> Encodes a string into the image
            For most purposes, deflate and mode should be omitted, since the
            defaults make the existence of the message harder to detect. A valid
            use case for deflate and mode would be to first encode a more easily
            detected decoy string, and only then the secret string: <code>
            image.encode(decoy, false, 1, function(image) { image.encode(secret,
            -1, callback); })</code>.
            (str, callback) -> <o> The image object (unmodified)
            (str, deflate, callback) -> <o> The image object (unmodified)
            (str, mode, callback) -> <o> The image object (unmodified)
            (str, deflate, mode, callback) -> <o> The image object (unmodified)
            (str, mode, deflate, callback) -> <o> The image object (unmodified)
            str <s> The string to be encoded
            callback <f> Callback function
                image <o> The image object (modified)
            deflate <b|true> If true, encode the string with deflate
            mode <n|0> Encoding mode
                If mode is between -7 and 0, the string will be encoded one bit
                per byte, as the number of bits within that byte set to 1,
                modulo 2, by flipping, if necessary, the most (mode -7) to least
                (mode 0) significant bit. If mode is between 1 and 255, the
                string will be encoded bitwise into all bits per byte that, in
                mode, are set to 1.
        @*/
        that.encode = function(str) {
            var callback = arguments[arguments.length - 1],
                deflate = Ox.isBoolean(arguments[1]) ? arguments[1]
                    : Ox.isBoolean(arguments[2]) ? arguments[2] : true,
                mode = Ox.isNumber(arguments[1]) ? arguments[1]
                    : Ox.isNumber(arguments[2]) ? arguments[2] : 0,
                b = 0, bin,
                // Array of bits per byte to be modified (0 is LSB)
                bits = mode < 1 ? [-mode] : Ox.map(Ox.range(8), function(bit) {
                    return mode & 1 << bit ? bit : null;
                }),
                cap = getCapacity(bits.length), len;
            Ox.print("CAPACITY", cap)
            // Compress the string
            str = Ox[deflate ? 'encodeDeflate' : 'encodeUTF8'](str);
            len = str.length;
            // Prefix the string with its length, as a four-byte value
            str = Ox.pad(Ox.encodeBase256(len), 4, '\u0000') + str;
            str.length > cap && error('encode');
            while (str.length < cap) {
                str += str.substr(4, len);
            }
            str = str.substr(0, Math.ceil(cap));
            // Create an array of bit values
            bin = Ox.flatten(Ox.map(str, function(chr) {
                return Ox.map(Ox.range(8), function(i) {
                    return chr.charCodeAt(0) >> 7 - i & 1;
                });
            }));
            b = 0;
            that.forEach(function(rgba, xy) {
                // If alpha is not 255, the RGB values may not be preserved
                if (rgba[3] == 255) {
                    var index = getIndex(xy);
                    Ox.loop(3, function(c) {
                        // fixme: use: var data = that.context.imageData.data[i + c]
                        var i = index + c;
                        Ox.forEach(bits, function(bit) {
                            if ((
                                mode < 1 
                                // If the number of bits set to 1, mod 2
                                ? Ox.sum(Ox.range(8).map(function(bit) {
                                    return +!!(self.data[i] & 1 << bit);
                                })) % 2
                                // or the one bit in question
                                : +!!(self.data[i] & 1 << bit)
                                // is not equal to the data bit
                            ) != bin[b++]) {
                                // then flip the bit
                                self.data[i] ^= 1 << bit;
                            }
                        });
                    });
                }
            });
            that.context.putImageData(that.imageData, 0, 0);
            callback(that);
            return that;
        };

        /*@
        decode <f> Decode encoded string
            (callback) -> <o> The image object (unmodified)
            (deflate, callback) -> <o> The image object (unmodified)
            (mode, callback) -> <o> The image object (unmodified)
            (deflate, mode, callback) -> <o> The image object (unmodified)
            (mode, deflate, callback) -> <o> The image object (unmodified)
            deflate <b|true> If true, decode the string with deflate
            mode <n|0> See encode method
            callback <f> Callback function
                image <o> The image object (modified)
        @*/
        that.decode = function() {
            var callback = arguments[arguments.length - 1],
                deflate = Ox.isBoolean(arguments[0]) ? arguments[0]
                    : Ox.isBoolean(arguments[1]) ? arguments[1] : true,
                mode = Ox.isNumber(arguments[0]) ? arguments[0]
                    : Ox.isNumber(arguments[1]) ? arguments[1] : 0,
                bin = '',
                // Array of bits per byte to be modified (0 is LSB)
                bits = mode < 1 ? [-mode] : Ox.map(Ox.range(8), function(b) {
                    return mode & 1 << b ? b : null;
                }),
                done = 0; len = 4, str = '';
            that.forEach(function(rgba, xy) {
                if (rgba[3] == 255) {
                    var index = getIndex(xy);
                    Ox.loop(3, function(c) {
                        var i = index + c;
                        Ox.forEach(bits, function(bit) {
                            bin += mode < 1
                                // Read the number of bits set to 1, mod 2
                                ? Ox.sum(Ox.range(8).map(function(bit) {
                                    return +!!(self.data[i] & 1 << bit);
                                })) % 2
                                // or the one bit in question
                                : +!!(self.data[i] & 1 << bit);
                            if (bin.length == 8) {
                                // Every 8 bits, add one byte to the string
                                str += Ox.char(parseInt(bin, 2));
                                bin = '';
                                if (str.length == len) {
                                    if (++done == 1) {
                                        // After 4 bytes, parse string as length
                                        len = Ox.decodeBase256(str);
                                        if (
                                            len <= 0 ||
                                            len > getCapacity(bits.length) - 4
                                        ) {
                                            error('decode');
                                        }
                                        str = '';
                                    } else {
                                        // After length more bytes, break
                                        return false;
                                    }
                                }
                            }
                        });
                        return done != 2;
                    });
                    return done != 2;
                }
            });
            try {
                if (deflate) {
                    Ox.decodeDeflate(str, callback);
                } else {
                    callback(Ox.decodeUTF8(str));
                }
            } catch (e) {
                error('decode');
            }
        }

        /*@
        filter <f> Pixel-wise filter function
            Undocumented, see source code
            (filter) -> <o> The image object
            (filter, bias) -> <o> The image object
            filter <[n]> Filter matrix
            bias <n> Bias
        @*/
        that.filter = function(filter, bias) {
            bias = bias || 0;
            var filterSize = Math.sqrt(filter.length),
                d = (filterSize - 1) / 2,
                imageData = that.context.createImageData(self.width, self.height),
                data = [];
            that.imageData = that.context.getImageData(0, 0, self.width, self.height);
            self.data = that.imageData.data;
            Ox.loop(0, self.data.length, 4, function(i) {
                var filterIndex = 0,
                    xy = getXY(i);
                Ox.loop(3, function(c) {
                    data[i + c] = 0;
                });
                Ox.loop(-d, d + 1, function(x) {
                    Ox.loop(-d, d + 1, function(y) {
                        var pixelIndex = getIndex(xy[0] + x, xy[1] + y);
                        Ox.loop(3, function(c) {
                            data[i + c] += self.data[pixelIndex + c] * filter[filterIndex];
                        });
                        filterIndex++;
                    });
                });
            });
            Ox.loop(0, self.data.length, 4, function(i) {
                Ox.loop(4, function(c) {
                    imageData.data[i + c] = c < 3
                        ? Ox.limit(Math.round(data[i + c] + bias), 0, 255)
                        : self.data[i + c];
                });
            });
            that.context.putImageData(imageData, 0, 0);
            that.imageData = imageData;
            self.data = data;
            return that;
        };

        /*@
        forEach <f> Pixel-wise forEach function
        (fn) -> <o> The image object
        fn <f> Iterator function
            rgba <[n]> RGBA values
            xy <[n]> XY coordinates
        @*/
        that.forEach = function(fn) {
            Ox.loop(0, self.data.length, 4, function(i) {
                return fn([
                    self.data[i], self.data[i + 1],
                    self.data[i + 2], self.data[i + 3]
                ], getXY(i));
            })
            return that;            
        };

        that.hue = function(val) {
            return that.map(function(rgba) {
                var hsl = Ox.hsl([rgba[0], rgba[1], rgba[2]]), rgb;
                hsl[0] = (hsl[0] + val) % 360;
                rgb = Ox.rgb(hsl);
                return Ox.merge(rgb, rgba[3]);
            });
        };

        //@ imageData <o> Image data object

        /*@
        invert <f> Apply invert filter
            () -> <o> The image object
        @*/
        that.invert = function() {
            return that.map(function(rgba) {
                return [255 - rgba[0], 255 - rgba[1], 255 - rgba[2], rgba[3]];
            });
        };

        /*@
        lightness <f> Apply lightness filter
            (val) -> <o> The image object
            val <n> Amount, from -1 (darkest) to 1 (lightest)
        @*/
        that.lightness = function(val) {
            return setSL('l', val);
        };

        /*@
        map <f> Pixel-wise map function
        (fn) -> <o> The image object
        fn <f> Iterator function
            rgba <[n]> RGBA values
            xy <[n]> XY coordinates
        @*/
        that.map = function(fn) {
            that.imageData = that.context.getImageData(0, 0, self.width, self.height);
            self.data = that.imageData.data;
            Ox.loop(0, self.data.length, 4, function(i) {
                fn([
                    self.data[i], self.data[i + 1],
                    self.data[i + 2], self.data[i + 3]
                ], getXY(i)).forEach(function(val, c) {
                    self.data[i + c] = val;
                });
            })
            that.context.putImageData(that.imageData, 0, 0);
            return that;
        };

        /*@
        mosaic <f> Apply mosaic filter
            (size) -> <o> The image object
            size <n> Mosaic size
        @*/
        that.mosaic = function(size) {
            that.forEach(function(rgba, xy) {
                if (xy[0] % size == 0 && xy[1] % size == 0) {
                    Ox.loop(size, function(x) {
                        Ox.loop(size, function(y) {
                            var hsl, rgb;
                            if ((x == 0 || y == 0) && !(x == size - 1 || y == size - 1)) {
                                that.pixel(xy[0] + x, xy[1] + y, rgba.map(function(c, i) {
                                    return i < 3 ? Math.min(c + 16, 255) : c;
                                }));
                            } else if ((x == size - 1 || y == size - 1) && !(x == 0 || y == 0)) {
                                that.pixel(xy[0] + x, xy[1] + y, rgba.map(function(c, i) {
                                    return i < 3 ? Math.max(c - 16, 0) : c;
                                }));
                            } else {
                                that.pixel(xy[0] + x, xy[1] + y, rgba);
                            }
                        });
                    });
                }
            });
            that.context.putImageData(that.imageData, 0, 0);
            return that;
        }

        /*@
        motionBlur <f> Apply motion blur filter
            () -> <o> The image object
        @*/
        that.motionBlur = function() {
            return that.filter([
                0.2, 0.0, 0.0, 0.0, 0.0,
                0.0, 0.2, 0.0, 0.0, 0.0,
                0.0, 0.0, 0.2, 0.0, 0.0,
                0.0, 0.0, 0.0, 0.2, 0.0,
                0.0, 0.0, 0.0, 0.0, 0.2
            ]);
        };

        /*@
        photocopy <f> Apply photocopy filter
            () -> <o> The image object
        @*/
        that.photocopy = function(val) {
            return that.saturation(-1).depth(1).blur(1);
        };

        /*@
        pixel <f> Get or set pixel values
            (x, y) -> <[n]> RGBA values
            (x, y, val) -> <o> The image object
            x <n> X coordinate
            y <n> Y coordinate
            val <[n]> RGBA values
        @*/
        that.pixel = function(x, y, val) {
            var i = getIndex(x, y);
            if (!val) {
                return Ox.range(4).map(function(c) {
                    return self.data[i + c];
                });
            } else {
                val.forEach(function(v, c) {
                   self.data[i + c] = v;
                });
            }
        }

        /*@
        posterize <f> Apply posterize filter
            () -> <o> The image object
        @*/
        that.posterize = function() {
            return that.blur(3).map(function(rgba) {
                return [
                    Math.floor(rgba[0] / 64) * 64,
                    Math.floor(rgba[1] / 64) * 64,
                    Math.floor(rgba[2] / 64) * 64,
                    rgba[3]
                ];
            });
        };

        that.resize = function(width, height) {
            // fixme: may not work this way
            that.canvas.attr({
                width: width,
                height: height
            });
            return that;
        }

        /*@
        saturation <f> Apply saturation filter
            (val) -> <o> The image object
            val <n> Amount, from -1 (lowest) to 1 (highest)
        @*/
        that.saturation = function(val) {
            return setSL('s', val);
        };

        /*@
        sharpen <f> Apply sharpen filter
            () -> <o> The image object
        @*/
        that.sharpen = function(val) {
            return that.filter([
                -1, -1, -1,
                -1, +9, -1,
                -1, -1, -1
            ]);
        };

        /*@
        solarize <f> Apply solarize filter
            () -> <o> The image object
        @*/
        that.solarize = function() {
            return that.map(function(rgba) {
                return [
                    rgba[0] < 128 ? rgba[0] : 255 - rgba[0],
                    rgba[1] < 128 ? rgba[1] : 255 - rgba[1],
                    rgba[2] < 128 ? rgba[2] : 255 - rgba[2],
                    rgba[3]
                ];
            });
        }

        /*@
        src <f> Get or set the image source
            () -> <s> Data URL
            (src) -> <o> Image object, with new source
            src <s> Image source (local, remote or data URL)
        @*/
        that.src = function() {
            if (arguments.length == 0) {
                return that.canvas[0].toDataURL();
            } else {
                var callback = arguments[1];
                self.src = arguments[0];
                self.image = new Image();
                self.image.onload = function() {
                    self.width = self.image.width;
                    self.height = self.image.height;
                    that.canvas.attr({
                        width: self.width,
                        height: self.height
                    });
                    that.context.drawImage(self.image, 0, 0);
                    that.imageData = that.context.getImageData(0, 0, self.width, self.height);
                    self.data = that.imageData.data;
                    callback && callback(that);
                }
                self.image.src = self.src;
                return that;
            }
        };

    };

    callback(true);

}

