(function() {

    function cap(width, height) {
        // returns maximum encoding capacity of an image
        return parseInt(width * height * 3/8) - 4;
    }

    function seek(data, px) {
        // returns this, or the next, opaque pixel
        while (data[px * 4 + 3] < 255) {
            if (++px * 4 == data.length) {
                throwPNGError('de');
            }
        }
        return px;
    }

    function xor(byte) {
        // returns "1"-bits-in-byte % 2
        // use: num.toString(2).replace(/0/g, '').length % 2
        var xor = 0;
        Ox.range(8).forEach(function(i) {
            xor ^= byte >> i & 1;
        });
        return xor;
    }

    function throwPNGError(str) {
        throw new RangeError(
            'PNG codec can\'t ' +
            (str == 'en' ? 'encode data' : 'decode image')
        );
    }

    function throwUTF8Error(byte, pos) {
        throw new RangeError(
            'UTF-8 codec can\'t decode byte 0x' +
            byte.toString(16).toUpperCase() + ' at position ' + pos
        );
    }

    /*@
    Ox.encodeBase26 <b> Encode a number as base26
        > Ox.encodeBase26(3758)
        'FOO'
    @*/
    Ox.encodeBase26 = function(num) {
        return Ox.map(num.toString(26), function(char) {
            return Ox.char(65 + parseInt(char, 26));
        }).join('');
    };

    /*@
    Ox.decodeBase26 <f> Decodes a base26-encoded number
        See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
        > Ox.decodeBase26('foo')
        3758
    @*/
    Ox.decodeBase26 = function(str) {
        return parseInt(Ox.map(str.toUpperCase(), function(char) {
            return (char.charCodeAt(0) - 65).toString(26);
        }).join(''), 26);
    };

    /*@
    Ox.encodeBase32 <b> Encode a number as base32
        See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
        > Ox.encodeBase32(15360)
        'F00'
        > Ox.encodeBase32(33819)
        '110V'
    @*/
    Ox.encodeBase32 = function(num) {
        return Ox.map(num.toString(32), function(char) {
            return Ox.BASE_32_DIGITS[parseInt(char, 32)];
        }).join('');
    };

    /*@
    Ox.decodeBase32 <f> Decodes a base32-encoded number
        See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
        > Ox.decodeBase32('foo')
        15360
        > Ox.decodeBase32('ILOU')
        33819
        > Ox.decodeBase32('?').toString()
        'NaN'
    @*/
    Ox.decodeBase32 = function(str) {
        return parseInt(Ox.map(str.toUpperCase(), function(char) {
            var index = Ox.BASE_32_DIGITS.indexOf(Ox.BASE_32_ALIASES[char] || char);
            return (index == -1 ? ' ' : index).toString(32);
        }).join(''), 32);
    };

    /*@
    Ox.encodeBase64 <f> Encode a number as base64
        > Ox.encodeBase64(32394)
        'foo'
    @*/
    Ox.encodeBase64 = function(num) {
        return btoa(Ox.encodeBase256(num)).replace(/=/g, '');
    };

    /*@
    Ox.decodeBase64 <f> Decodes a base64-encoded number
        > Ox.decodeBase64('foo')
        32394
    @*/
    Ox.decodeBase64 = function(str) {
        return Ox.decodeBase256(atob(str));
    };

    /*@
    Ox.encodeBase128 <f> Encode a number as base128
        > Ox.encodeBase128(1685487)
        'foo'
    @*/
    Ox.encodeBase128 = function(num) {
        var str = '';
        while (num) {
            str = Ox.char(num & 127) + str;
            num >>= 7;
        }
        return str || '0';
    };

    /*@
    Ox.decodeBase128 <f> Decode a base128-encoded number
        > Ox.decodeBase128('foo')
        1685487
    @*/
    Ox.decodeBase128 = function(str) {
        var num = 0, len = str.length;
        Ox.forEach(str, function(char, i) {
            num += char.charCodeAt(0) << (len - i - 1) * 7;
        });
        return num;
    };

    /*@
    Ox.encodeBase256 <f> Encode a number as base256
        > Ox.encodeBase256(6713199)
        'foo'
    @*/
    Ox.encodeBase256 = function(num) {
        var str = '';
        while (num) {
            str = Ox.char(num & 255) + str;
            num >>= 8;
        }
        return str;
    };

    /*@
    Ox.decodeBase256 <f> Decode a base256-encoded number
        > Ox.decodeBase256('foo')
        6713199
    @*/
    Ox.decodeBase256 = function(str) {
        var num = 0, len = str.length;
        Ox.forEach(str, function(char, i) {
            num += char.charCodeAt(0) << (len - i - 1) * 8;
        });
        return num;
    };

    /*@
    Ox.encodeDeflate <f> Encodes a string, using deflate
        Since PNGs are deflate-encoded, the <code>canvas</code> object's
        <code>toDataURL</code> method provides an efficient implementation.
        The string is encoded as UTF-8 and written to the RGB channels of a
        canvas element, then the PNG dataURL is decoded from base64, and some
        head, tail and chunk names are removed.
        (str) -> <s> The encoded string
            str <s> The string to be encoded
        # Test with: Ox.decodeDeflate(Ox.encodeDeflate('foo'), alert)
    @*/
    
    Ox.encodeDeflate = function(str, callback) {
        // Make sure we can encode the full unicode range of characters.
        str = Ox.encodeUTF8(str);
        // We can only safely write to RGB, so we need 1 pixel for 3 bytes.
        // The string length may not be a multiple of 3, so we need to encode
        // the number of padding bytes (1 byte), the string, and non-0-bytes
        // as padding, so that the combined length becomes a multiple of 3.
        var len = 1 + str.length, c = Ox.canvas(Math.ceil(len / 3), 1),
            data, idat, pad = (3 - len % 3) % 3;
        str = Ox.char(pad) + str + Ox.repeat('\u00FF', pad);
        Ox.loop(c.data.length, function(i) {
            // Write character codes into RGB, and 255 into ALPHA
            c.data[i] = i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255;
        });
        c.context.putImageData(c.imageData, 0, 0);
        // Get the PNG data from the data URL and decode it from base64.
        str = atob(c.canvas.toDataURL().split(',')[1]);
        // Discard bytes 0 to 15 (8 bytes PNG signature, 4 bytes IHDR length, 4
        // bytes IHDR name), keep bytes 16 to 19 (width), discard bytes 20 to 29
        // (4 bytes height, 5 bytes flags), keep bytes 29 to 32 (IHDR checksum),
        // keep the rest (IDAT chunks), discard the last 12 bytes (IEND chunk).
        data = str.substr(16, 4) + str.substr(29, 4);
        idat = str.substr(33, str.length - 45);
        while (idat) {
            // Each IDAT chunk is 4 bytes length, 4 bytes name, length bytes
            // data and 4 bytes checksum. We can discard the name parts.
            len = idat.substr(0, 4);
            data += len + idat.substr(8, 4 + (len = Ox.decodeBase256(len)));
            idat = idat.substr(12 + len);
        }
        callback && callback(data);
        return data;
    };

    /*@
    Ox.decodeDeflate <f> Decodes an deflate-encoded string
        Since PNGs are deflate-encoded, the <code>canvas</code> object's
        <code>drawImage</code> method provides an efficient implementation. The
        string will be wrapped as a PNG dataURL, encoded as base64, and drawn
        onto a canvas element, then the RGB channels will be read, and the
        result will be decoded from UTF8.
        (str) -> <u> undefined
            str <s> The string to be decoded
            callback <f> Callback function
                str <s> The decoded string
    @*/

    Ox.decodeDeflate = function(str, callback) {
        var image = new Image(),
            // PNG file signature and IHDR chunk
            data = '\u0089PNG\r\n\u001A\n\u0000\u0000\u0000\u000DIHDR'
                + str.substr(0, 4) + '\u0000\u0000\u0000\u0001'
                + '\u0008\u0006\u0000\u0000\u0000' + str.substr(4, 4),
            // IDAT chunks
            idat = str.substr(8), len;
        function error() {
            throw new RangeError('Deflate codec can\'t decode data.');
        }
        while (idat) {
            // Reinsert the IDAT chunk names
            len = idat.substr(0, 4);
            data += len + 'IDAT' + idat.substr(4, 4 + (len = Ox.decodeBase256(len)));
            idat = idat.substr(8 + len);
        }
        // IEND chunk
        data += '\u0000\u0000\u0000\u0000IEND\u00AE\u0042\u0060\u0082';
        // Unfortunately, we can't synchronously set the source of an image,
        // draw it onto a canvas, and read its data.
        image.onload = function() {
            str = Ox.makeArray(Ox.canvas(image).data).map(function(v, i) {
                // Read one character per RGB byte, ignore ALPHA.
                return i % 4 < 3 ? Ox.char(v) : '';
            }).join('');
            try {
                // Parse the first byte as number of bytes to chop at the end,
                // and the rest, without these bytes, as an UTF8-encoded string.
                str = Ox.decodeUTF8(str.substr(1, str.length - 1 - str.charCodeAt(0)))
            } catch (e) {
                error();
            }
            callback(str);
        }
        image.onerror = error;
        image.src = 'data:image/png;base64,' + btoa(data);
    };

    /*@
    Ox.encodeHTML <f> HTML-encodes a string
        > Ox.encodeHTML('\'<"&">\'')
        '&apos;&lt;&quot;&amp;&quot;&gt;&apos;'
        > Ox.encodeHTML('äbçdê')
        '&#x00E4;b&#x00E7;d&#x00EA;'
    @*/
    Ox.encodeHTML = function(str) {
        return Ox.map(str.toString(), function(v) {
            var code = v.charCodeAt(0);
            return code < 128 ? (v in Ox.HTML_ENTITIES ? Ox.HTML_ENTITIES[v] : v) :
                '&#x' + Ox.pad(code.toString(16).toUpperCase(), 4) + ';';
        }).join('');
    };

    /*@
    Ox.decodeHTML <f> Decodes an HTML-encoded string
        > Ox.decodeHTML('&apos;&lt;&quot;&amp;&quot;&gt;&apos;')
        '\'<"&">\''
        > Ox.decodeHTML('&#x0027;&#x003C;&#x0022;&#x0026;&#x0022;&#x003E;&#x0027;')
        '\'<"&">\''
        > Ox.decodeHTML('&auml;b&ccedil;d&ecirc;')
        'äbçdê'
        > Ox.decodeHTML('&#x00E4;b&#x00E7;d&#x00EA;')
        'äbçdê'
        > Ox.decodeHTML('<b>bold</b>')
        '<b>bold</b>'
    @*/
    Ox.decodeHTML = function(str) {
        // relies on dom, but shorter than using this:
        // http://www.w3.org/TR/html5/named-character-references.html 
        return Ox.decodeHTMLEntities(Ox.element('<div>').html(str).html());
    };

    Ox.encodeHTMLEntities = function(str) {
        return str.replace(
            new RegExp('(' + Object.keys(Ox.HTML_ENTITIES).join('|') + ')', 'g'),
            function(match) {
                return Ox.HTML_ENTITIES[match];
            }
        );
    };

    Ox.decodeHTMLEntities = function(str) {
        return str.replace(
            new RegExp('(' + Ox.values(Ox.HTML_ENTITIES).join('|') + ')', 'g'),
            function(match) {
                return Ox.keyOf(Ox.HTML_ENTITIES, match);
            }
        );
    };

    /*@
    Ox.encodePNG <f> Encodes a string into an image, returns a new image
        The string is compressed with deflate (by proxy of canvas), prefixed
        with its length (four bytes), and encoded bitwise into the red, green
        and blue bytes of all fully opaque pixels of the image, by flipping, if
        necessary, the least significant bit, so that for every byte, the total
        number of bits set to to 1, modulo 2, is the bit that we are encoding.
    (img, src) -> <e> An image into which the string has been encoded
        img <e> Any JavaScript PNG image object
        str <s> The string to be encoded
    @*/
    
    Ox.encodePNG = function(img, str) {
        var c = Ox.canvas(img), i = 0;
        // Compress the string
        str = Ox.encodeDeflate(str);
        // Prefix the string with its length, as a four-byte value
        str = Ox.pad(Ox.encodeBase256(str.length), 4, Ox.char(0)) + str;
        // Create an array of bit values
        Ox.forEach(Ox.flatten(Ox.map(str, function(chr) {
            return Ox.map(Ox.range(8), function(i) {
                return chr.charCodeAt(0) >> 7 - i & 1;
            });
        })), function(bit) {
            // Skip all pixels that are not fully opaque
            while (i < c.data.length && i % 4 == 0 && c.data[i + 3] < 255) {
                i += 4;
            }
            if (i == c.data.length) {
                throw new RangeError('PNG codec can\'t encode data');
            }
            // If the number of bits set to one, modulo 2 is equal to the bit,
            // do nothing, otherwise, flip the least significant bit.
            c.data[i] += c.data[i].toString(2).replace(/0/g, '').length % 2
                == bit ? 0 : c.data[i] % 2 ? -1 : 1;
            i++;
        });
        c.context.putImageData(c.imageData, 0, 0);
        img = new Image();
        img.src = c.canvas.toDataURL();
        return img;
        /*
        wishlist:
        - only use deflate if it actually shortens the message
        - encode a decoy message into the least significant bit
          (and flip the second least significant bit, if at all)
        - write an extra png chunk containing some key
        */
    };

    /*@
    Ox.decodePNG <f> Decodes an image, returns a string
        For every red, green and blue byte of every fully opaque pixel of the
        image, one bit, namely the number of bits of the byte set to one, modulo
        2, is being read, the result being the string, prefixed with its length
        (four bytes), which is decompressed with deflate (by proxy of canvas).
        (img, callback) -> <u> undefined
            img <e> The image into which the string has been encoded
            callback <f> Callback function
                str <s> The decoded string
    @*/
    
    Ox.decodePNG = function(img, callback) {
        var bits = '', data = Ox.canvas(img).data, flag = false, i = 0,
            len = 4, str = '';
        while (len) {
            // Skip all pixels that are not fully opaque
            while (i < data.length && i % 4 == 0 && data[i + 3] < 255) {
                i += 4;
            }
            if (i == data.length) {
                break;
            }
            // Read the number of bits set to one, modulo 2
            bits += data[i].toString(2).replace(/0/g, '').length % 2;
            if (++i % 8 == 0) {
                // Every 8 bits, add one byte
                str += Ox.char(parseInt(bits, 2));
                bits = '';
                // When length reaches 0 for the first time,
                // decode the string and treat it as the new length
                if (--len == 0 && !flag) {
                    flag = true;
                    len = Ox.decodeBase256(str);
                    str = '';
                }
            }
        }
        try {
            Ox.decodeDeflate(str, callback);
        } catch (e) {
            throw new RangeError('PNG codec can\'t decode image');
        }
    };

    /*@
    Ox.encodeUTF8 <f> Encodes a string as UTF-8
        see http://en.wikipedia.org/wiki/UTF-8
        (string) -> <s> UTF-8 encoded string
        string <s> Any string
        > Ox.encodeUTF8("YES")
        "YES"
        > Ox.encodeUTF8("¥€$")
        "\u00C2\u00A5\u00E2\u0082\u00AC\u0024"
    @*/
    Ox.encodeUTF8 = function(str) {
        return Ox.map(str, function(chr) {
            var code = chr.charCodeAt(0),
                str = '';
            if (code < 128) {
                str = chr;
            } else if (code < 2048) {
                str = String.fromCharCode(code >> 6 | 192) +
                    String.fromCharCode(code & 63 | 128);
            } else {
                str = String.fromCharCode(code >> 12 | 224) +
                    String.fromCharCode(code >> 6 & 63 | 128) +
                    String.fromCharCode(code & 63 | 128);
            }
            return str;
        }).join('');
    };

    /*@
    Ox.decodeUTF8 <f> Decodes an UTF-8-encoded string
        see http://en.wikipedia.org/wiki/UTF-8
        (utf8) -> <s> string
        utf8 <s> Any UTF-8-encoded string
        > Ox.decodeUTF8('YES')
        'YES'
        > Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024')
        '¥€$'
    @*/
    Ox.decodeUTF8 = function(str) {
        var bytes = Ox.map(str, function(v) {
                return v.charCodeAt(0);
            }),
            i = 0,
            len = str.length,
            str = '';
        while (i < len) {
            if (bytes[i] <= 128) {
                str += String.fromCharCode(bytes[i]);
                i++;
            } else if (
                bytes[i] >= 192 && bytes[i] < 240 &&
                i < len - (bytes[i] < 224 ? 1 : 2)
            ) {
                if (bytes[i + 1] >= 128 && bytes[i + 1] < 192) {
                    if (bytes[i] < 224) {
                        str += String.fromCharCode((bytes[i] & 31) << 6 |
                            bytes[i + 1] & 63);
                        i += 2;
                    } else if (bytes[i + 2] >= 128 && bytes[i + 2] < 192) {
                        str += String.fromCharCode((bytes[i] & 15) << 12 |
                            (bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63);
                        i += 3;
                    } else {
                        throwUTF8Error(bytes[i + 2], i + 2);
                    }
                } else {
                    throwUTF8Error(bytes[i + 1], i + 1);
                }
            } else {
                throwUTF8Error(bytes[i], i);
            }
        }
        return str;
    };

})();