/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

(function(global, undefined) { "use strict";
    var POW_2_24 = 5.960464477539063e-8,
        POW_2_32 = 4294967296,
        POW_2_53 = 9007199254740992;

    function encode(value) {
        var data = new ArrayBuffer(256);
        var dataView = new DataView(data);
        var lastLength;
        var offset = 0;

        function prepareWrite(length) {
            var newByteLength = data.byteLength;
            var requiredLength = offset + length;
            while (newByteLength < requiredLength)
                newByteLength <<= 1;
            if (newByteLength !== data.byteLength) {
                var oldDataView = dataView;
                data = new ArrayBuffer(newByteLength);
                dataView = new DataView(data);
                var uint32count = (offset + 3) >> 2;
                for (var i = 0; i < uint32count; ++i)
                    dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
            }

            lastLength = length;
            return dataView;
        }
        function commitWrite() {
            offset += lastLength;
        }
        function writeFloat64(value) {
            commitWrite(prepareWrite(8).setFloat64(offset, value));
        }
        function writeUint8(value) {
            commitWrite(prepareWrite(1).setUint8(offset, value));
        }
        function writeUint8Array(value) {
            var dataView = prepareWrite(value.length);
            for (var i = 0; i < value.length; ++i)
                dataView.setUint8(offset + i, value[i]);
            commitWrite();
        }
        function writeUint16(value) {
            commitWrite(prepareWrite(2).setUint16(offset, value));
        }
        function writeUint32(value) {
            commitWrite(prepareWrite(4).setUint32(offset, value));
        }
        function writeUint64(value) {
            var low = value % POW_2_32;
            var high = (value - low) / POW_2_32;
            var dataView = prepareWrite(8);
            dataView.setUint32(offset, high);
            dataView.setUint32(offset + 4, low);
            commitWrite();
        }
        function writeTypeAndLength(type, length) {
            if (length < 24) {
                writeUint8(type << 5 | length);
            } else if (length < 0x100) {
                writeUint8(type << 5 | 24);
                writeUint8(length);
            } else if (length < 0x10000) {
                writeUint8(type << 5 | 25);
                writeUint16(length);
            } else if (length < 0x100000000) {
                writeUint8(type << 5 | 26);
                writeUint32(length);
            } else {
                writeUint8(type << 5 | 27);
                writeUint64(length);
            }
        }

        function encodeItem(value) {
            var i;

            if (value === false)
                return writeUint8(0xf4);
            if (value === true)
                return writeUint8(0xf5);
            if (value === null)
                return writeUint8(0xf6);
            if (value === undefined)
                return writeUint8(0xf7);

            switch (typeof value) {
                case "number":
                    if (Math.floor(value) === value) {
                        if (0 <= value && value <= POW_2_53)
                            return writeTypeAndLength(0, value);
                        if (-POW_2_53 <= value && value < 0)
                            return writeTypeAndLength(1, -(value + 1));
                    }
                    writeUint8(0xfb);
                    return writeFloat64(value);

                case "string":
                    var utf8data = [];
                    for (i = 0; i < value.length; ++i) {
                        var charCode = value.charCodeAt(i);
                        if (charCode < 0x80) {
                            utf8data.push(charCode);
                        } else if (charCode < 0x800) {
                            utf8data.push(0xc0 | charCode >> 6);
                            utf8data.push(0x80 | charCode & 0x3f);
                        } else if (charCode < 0xd800) {
                            utf8data.push(0xe0 | charCode >> 12);
                            utf8data.push(0x80 | (charCode >> 6)  & 0x3f);
                            utf8data.push(0x80 | charCode & 0x3f);
                        } else {
                            charCode = (charCode & 0x3ff) << 10;
                            charCode |= value.charCodeAt(++i) & 0x3ff;
                            charCode += 0x10000;

                            utf8data.push(0xf0 | charCode >> 18);
                            utf8data.push(0x80 | (charCode >> 12)  & 0x3f);
                            utf8data.push(0x80 | (charCode >> 6)  & 0x3f);
                            utf8data.push(0x80 | charCode & 0x3f);
                        }
                    }

                    writeTypeAndLength(3, utf8data.length);
                    return writeUint8Array(utf8data);

                default:
                    var length;
                    if (Array.isArray(value)) {
                        length = value.length;
                        writeTypeAndLength(4, length);
                        for (i = 0; i < length; ++i)
                            encodeItem(value[i]);
                    } else if (value instanceof Uint8Array) {
                        writeTypeAndLength(2, value.length);
                        writeUint8Array(value);
                    } else {
                        var keys = Object.keys(value);
                        length = keys.length;
                        writeTypeAndLength(5, length);
                        for (i = 0; i < length; ++i) {
                            var key = keys[i];
                            encodeItem(key);
                            encodeItem(value[key]);
                        }
                    }
            }
        }

        encodeItem(value);

        if ("slice" in data)
            return data.slice(0, offset);

        var ret = new ArrayBuffer(offset);
        var retView = new DataView(ret);
        for (var i = 0; i < offset; ++i)
            retView.setUint8(i, dataView.getUint8(i));
        return ret;
    }

    function decode(data, tagger, simpleValue) {
        var dataView = new DataView(data);
        var offset = 0;

        if (typeof tagger !== "function")
            tagger = function(value) { return value; };
        if (typeof simpleValue !== "function")
            simpleValue = function() { return undefined; };

        function commitRead(length, value) {
            offset += length;
            return value;
        }
        function readArrayBuffer(length) {
            return commitRead(length, new Uint8Array(data, offset, length));
        }
        function readFloat16() {
            var tempArrayBuffer = new ArrayBuffer(4);
            var tempDataView = new DataView(tempArrayBuffer);
            var value = readUint16();

            var sign = value & 0x8000;
            var exponent = value & 0x7c00;
            var fraction = value & 0x03ff;

            if (exponent === 0x7c00)
                exponent = 0xff << 10;
            else if (exponent !== 0)
                exponent += (127 - 15) << 10;
            else if (fraction !== 0)
                return (sign ? -1 : 1) * fraction * POW_2_24;

            tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
            return tempDataView.getFloat32(0);
        }
        function readFloat32() {
            return commitRead(4, dataView.getFloat32(offset));
        }
        function readFloat64() {
            return commitRead(8, dataView.getFloat64(offset));
        }
        function readUint8() {
            return commitRead(1, dataView.getUint8(offset));
        }
        function readUint16() {
            return commitRead(2, dataView.getUint16(offset));
        }
        function readUint32() {
            return commitRead(4, dataView.getUint32(offset));
        }
        function readUint64() {
            return readUint32() * POW_2_32 + readUint32();
        }
        function readBreak() {
            if (dataView.getUint8(offset) !== 0xff)
                return false;
            offset += 1;
            return true;
        }
        function readLength(additionalInformation) {
            if (additionalInformation < 24)
                return additionalInformation;
            if (additionalInformation === 24)
                return readUint8();
            if (additionalInformation === 25)
                return readUint16();
            if (additionalInformation === 26)
                return readUint32();
            if (additionalInformation === 27)
                return readUint64();
            if (additionalInformation === 31)
                return -1;
            throw "Invalid length encoding";
        }
        function readIndefiniteStringLength(majorType) {
            var initialByte = readUint8();
            if (initialByte === 0xff)
                return -1;
            var length = readLength(initialByte & 0x1f);
            if (length < 0 || (initialByte >> 5) !== majorType)
                throw "Invalid indefinite length element";
            return length;
        }

        function appendUtf16Data(utf16data, length) {
            for (var i = 0; i < length; ++i) {
                var value = readUint8();
                if (value & 0x80) {
                    if (value < 0xe0) {
                        value = (value & 0x1f) <<  6
                            | (readUint8() & 0x3f);
                        length -= 1;
                    } else if (value < 0xf0) {
                        value = (value & 0x0f) << 12
                            | (readUint8() & 0x3f) << 6
                            | (readUint8() & 0x3f);
                        length -= 2;
                    } else {
                        value = (value & 0x0f) << 18
                            | (readUint8() & 0x3f) << 12
                            | (readUint8() & 0x3f) << 6
                            | (readUint8() & 0x3f);
                        length -= 3;
                    }
                }

                if (value < 0x10000) {
                    utf16data.push(value);
                } else {
                    value -= 0x10000;
                    utf16data.push(0xd800 | (value >> 10));
                    utf16data.push(0xdc00 | (value & 0x3ff));
                }
            }
        }

        function decodeItem() {
            var initialByte = readUint8();
            var majorType = initialByte >> 5;
            var additionalInformation = initialByte & 0x1f;
            var i;
            var length;

            if (majorType === 7) {
                switch (additionalInformation) {
                    case 25:
                        return readFloat16();
                    case 26:
                        return readFloat32();
                    case 27:
                        return readFloat64();
                }
            }

            length = readLength(additionalInformation);
            if (length < 0 && (majorType < 2 || 6 < majorType))
                throw "Invalid length";

            switch (majorType) {
                case 0:
                    return length;
                case 1:
                    return -1 - length;
                case 2:
                    if (length < 0) {
                        var elements = [];
                        var fullArrayLength = 0;
                        while ((length = readIndefiniteStringLength(majorType)) >= 0) {
                            fullArrayLength += length;
                            elements.push(readArrayBuffer(length));
                        }
                        var fullArray = new Uint8Array(fullArrayLength);
                        var fullArrayOffset = 0;
                        for (i = 0; i < elements.length; ++i) {
                            fullArray.set(elements[i], fullArrayOffset);
                            fullArrayOffset += elements[i].length;
                        }
                        return fullArray;
                    }
                    return readArrayBuffer(length);
                case 3:
                    var utf16data = [];
                    if (length < 0) {
                        while ((length = readIndefiniteStringLength(majorType)) >= 0)
                            appendUtf16Data(utf16data, length);
                    } else
                        appendUtf16Data(utf16data, length);
                    return String.fromCharCode.apply(null, utf16data);
                case 4:
                    var retArray;
                    if (length < 0) {
                        retArray = [];
                        while (!readBreak())
                            retArray.push(decodeItem());
                    } else {
                        retArray = new Array(length);
                        for (i = 0; i < length; ++i)
                            retArray[i] = decodeItem();
                    }
                    return retArray;
                case 5:
                    var retObject = {};
                    for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
                        var key = decodeItem();
                        retObject[key] = decodeItem();
                    }
                    return retObject;
                case 6:
                    return tagger(decodeItem(), length);
                case 7:
                    switch (length) {
                        case 20:
                            return false;
                        case 21:
                            return true;
                        case 22:
                            return null;
                        case 23:
                            return undefined;
                        default:
                            return simpleValue(length);
                    }
            }
        }

        var ret = decodeItem();
        if (offset !== data.byteLength)
            throw "Remaining bytes";
        return ret;
    }

    var obj = { encode: encode, decode: decode };

    if (typeof define === "function" && define.amd)
        define("cbor/cbor", obj);
    else if (typeof module !== "undefined" && module.exports)
        module.exports = obj;
    else if (!global.CBOR)
        global.CBOR = obj;

})(this);