"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var buffer_1 = require("buffer");
var tap_stream_1 = require("../client/impl/frame/tap-stream");
var logger_1 = require("../logger");
var format_1 = require("./format");
var kaitai_stream_writer_1 = require("./kaitai/kaitai-stream-writer");
var logger = logger_1.default('TLV');
var TLV;
(function (TLV) {
    var decoders = {
        String: function (payload, offset, len) {
            return payload.toString('utf8', offset, offset + len);
        },
        Integer: function (payload, offset, len) {
            return payload.readIntBE(offset, len);
        },
        Float: function (payload, offset, len) {
            switch (len) {
                case 4:
                    return payload.readFloatBE(offset);
                case 8:
                    return payload.readDoubleBE(offset);
                default:
                    throw new Error("Invalid length for float: " + len);
            }
        },
        Boolean: function (payload, offset, len) {
            return payload.readUInt8(offset) ? true : false;
        },
        Opaque: function (payload, offset, len) {
            if (payload.length < offset + len) {
                throw new Error("Buffer too small. Expecting " + len + " bytes but only " + (payload.length - offset) + " avaiable(s). Offset of " + offset);
            }
            var buf = buffer_1.Buffer.alloc(len);
            payload.copy(buf, 0, offset, offset + len);
            return buf;
        },
        Time: function (payload, offset, len) {
            var timestamp = payload.readIntBE(offset, len);
            return new Date(timestamp * 1e3);
        },
    };
    var DataType;
    (function (DataType) {
        DataType["String"] = "String";
        DataType["Integer"] = "Integer";
        DataType["Float"] = "Float";
        DataType["Boolean"] = "Boolean";
        DataType["Opaque"] = "Opaque";
        DataType["Time"] = "Time";
        DataType["Array"] = "Array";
    })(DataType = TLV.DataType || (TLV.DataType = {}));
    var IdentifierType;
    (function (IdentifierType) {
        IdentifierType[IdentifierType["OBJECT_INSTANCE"] = 0] = "OBJECT_INSTANCE";
        IdentifierType[IdentifierType["RESOURCE_INSTANCE"] = 1] = "RESOURCE_INSTANCE";
        IdentifierType[IdentifierType["MULTIPLE_RESOURCE"] = 2] = "MULTIPLE_RESOURCE";
        IdentifierType[IdentifierType["RESOURCE_VALUE"] = 3] = "RESOURCE_VALUE";
    })(IdentifierType = TLV.IdentifierType || (TLV.IdentifierType = {}));
    function tlvType(type, id, len) {
        var byte = type << 6;
        if (id > 0xff) {
            byte |= 0x1 << 5;
        }
        if (len < 8) {
            byte |= len;
        }
        else {
            byte |= length(len) << 3;
        }
        return byte;
    }
    TLV.tlvType = tlvType;
    var Parser = /** @class */ (function () {
        function Parser(schema) {
            this.position = 0;
            this.schema = schema;
            if (this.schema) {
                throw new Error("Schema are not supported yet");
            }
        }
        /**
         *
         * @param buf
         * @param offset
         * @param tlv
         */
        Parser.prototype.readHeader = function () {
            var type, id, len;
            var header;
            header = this.buffer.readUInt8(this.position);
            this.position += 1;
            type = header >>> 6;
            if (header & 0x20) {
                id = this.buffer.readUInt16BE(this.position);
                this.position += 2;
            }
            else {
                id = this.buffer.readUInt8(this.position);
                this.position += 1;
            }
            len = header & 0x7;
            if (!len) {
                switch ((header & 0x18) >>> 3) {
                    case 1:
                        len = this.buffer.readUInt8(this.position);
                        this.position += 1;
                        break;
                    case 2:
                        len = this.buffer.readUInt16BE(this.position);
                        this.position += 2;
                        break;
                    case 3:
                        len = this.buffer.readUInt16BE(this.position);
                        len = this.buffer.readUInt8(this.position);
                        this.position += 3;
                        break;
                }
            }
            var result = {
                id: id,
                type: type,
                len: len
            };
            this.debug("\t-Found header: ", result);
            return result;
        };
        Parser.prototype.readNextRecord = function () {
            var header = this.readHeader();
            var key = header.id.toString();
            if (header.type == IdentifierType.RESOURCE_VALUE || header.type == IdentifierType.RESOURCE_INSTANCE) {
                this.debug("\t-Resource value readRecords with", header);
                var payload = buffer_1.Buffer.alloc(header.len);
                this.buffer.copy(payload, 0, this.position, this.position + header.len);
                this.position += header.len;
                return {
                    header: header,
                    payload: payload
                };
            }
            else {
                this.debug("\t-Multiple resources found! readRecords with key " + key);
                var children = this.readAllRecords(this.position + header.len);
                return {
                    header: header,
                    children: children
                };
            }
        };
        // readNextRecord() {
        //     this.debug(`Parsing record`);
        //     var key: string, type: DataType | Array<any>;
        //     let header = this.readHeader();
        //     key = this.schema.id[header.id];
        //     // skip resources not defined in schema.
        //     if (!key) {
        //         this.debug('\t-Resource not defined in schema: %s', key);
        //         this.position += header.len;
        //         return;
        //     }
        //     this.debug(`\t-Key=${key}`);
        //     type = this.schema.resources[key].type;
        //     if (Array.isArray(type)) {
        //         this.debug(`\t-Reading array`);
        //         var end = this.position + header.len;
        //         var itemHeader: Header = {} as any;
        //         let itemValue: any;
        //         result[key] = [];
        //         while (this.position < end) {
        //             itemHeader = this.readHeader();
        //             itemValue = this.append(itemHeader, type[0]);
        //             result[key].push(itemValue); // TODO
        //         }
        //     } else {
        //         result[key] = this.append(header, type);
        //     }
        // }
        Parser.prototype.append = function (header, type) {
            this.debug("\t-Append " + type + " header=", header);
            if (!(type in decoders)) {
                throw new Error("Decoder type " + type + " not found");
            }
            var result = (decoders[type])(this.buffer, this.position, header.len);
            this.debug("\t-Appending value", result);
            this.position += header.len;
            return result;
        };
        Parser.prototype.readAllRecords = function (endPosition) {
            var result = [];
            this.debug("Parsing records until position " + endPosition);
            while (this.position < endPosition) {
                result.push(this.readNextRecord());
            }
            return result;
        };
        Parser.prototype.parse = function (data) {
            this.position = 0;
            this.buffer = buffer_1.Buffer.from(tap_stream_1.typedArrayToBuffer(data));
            return this.readNextRecord();
        };
        Parser.prototype.debug = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            logger.debug.apply(logger, args.concat(["[pos=" + this.position + "/" + (this.buffer.length - 1) + ", remaining buffer=" + format_1.FormatHelper.toHexString(this.buffer.slice(this.position)) + "]"]));
        };
        return Parser;
    }());
    TLV.Parser = Parser;
    var Serializer = /** @class */ (function () {
        function Serializer() {
        }
        Serializer.prototype.serialize = function (obj, stream) {
            stream = stream || new kaitai_stream_writer_1.KaitaiStreamWriter();
            this.writeHeader(stream, obj.header.type, obj.header.id, obj.header.len);
            if (obj.payload) {
                stream.writeBytes(obj.payload);
            }
            if (obj.children) {
                for (var _i = 0, _a = obj.children; _i < _a.length; _i++) {
                    var node = _a[_i];
                    this.serialize(node, stream);
                }
            }
            return stream.toBytes;
        };
        Serializer.prototype.writeHeader = function (stream, idType, id, len) {
            var type = tlvType(idType, id, len);
            /* type (8-bits masked field) */
            stream.writeU(type, 1);
            /* identifier (8-bit or 16-bit UInt) */
            if (type & 0x20) {
                stream.writeU(id, 2);
            }
            else {
                stream.writeU(id, 1);
            }
            /* length (0-24-bit UInt) */
            if (type & 0x18) {
                switch (length(len)) {
                    case 3:
                        stream.writeU(len >>> 0x10 & 0xff, 1);
                    /* falls through */
                    case 2:
                        stream.writeU(len >>> 0x08 & 0xff, 1);
                    /* falls through */
                    case 1:
                        stream.writeU(len & 0xff, 1);
                        break;
                    default:
                        throw new Error('Invalid resource `' + id + '`');
                }
            }
        };
        Serializer.prototype.serializeObj = function (obj, schema) {
            var buf = buffer_1.Buffer.alloc(16 * 1024 /*1024*/);
            var keys = Object.keys(obj);
            if (schema) {
                schema.validate(obj);
            }
            var len = 0;
            for (var i = 0; i < keys.length; ++i) {
                var key = keys[i];
                // skip resources not defined in schema
                if (!schema || !schema.resources[key]) {
                    continue;
                }
                len = writeRecordWithSchema(obj, buf, len, key);
            }
            console.info(buf, len);
            return buf.slice(0, len);
        };
        return Serializer;
    }());
    TLV.Serializer = Serializer;
    function writeHeader(buf, offset, idType, id, len) {
        var type = tlvType(idType, id, len);
        /* type (8-bits masked fielsd) */
        buf.writeUInt8(type, offset);
        offset += 1;
        /* identifier (8-bit or 16-bit UInt) */
        if (type & 0x20) {
            buf.writeUInt16BE(id, offset);
            offset += 2;
        }
        else {
            buf.writeUInt8(id, offset);
            offset += 1;
        }
        /* length (0-24-bit UInt) */
        if (type & 0x18) {
            switch (length(len)) {
                case 3:
                    buf.writeUInt8(len >>> 0x10 & 0xff, offset);
                    offset += 1;
                /* falls through */
                case 2:
                    buf.writeUInt8(len >>> 0x08 & 0xff, offset);
                    offset += 1;
                /* falls through */
                case 1:
                    buf.writeUInt8(len & 0xff, offset);
                    offset += 1;
                    break;
                default:
                    throw new Error('Invalid resource `' + id + '`');
            }
        }
        return offset;
    }
    function writeData(buf, offset, len, value, type) {
        // if (type){
        //     switch (type){
        //         case DataType.String:
        //             stream.write();
        //         case DataType.Boolean:
        //             stream.writeB
        //     }
        // }
        var ValueSerializerMap = {
            String: function () {
                buf.write(value, offset, buf.length - offset, 'utf8');
                return offset += len;
            },
            Integer: function () {
                buf.writeIntBE(value, offset, len);
                return offset += len;
            },
            Float: function () {
                buf.writeDoubleBE(value, offset);
                return offset += len;
            },
            Boolean: function () {
                buf.writeInt8(value, offset);
                return offset += len;
            },
            Opaque: function () {
                value.copy(buf, offset);
                return offset += len;
            },
            Time: function () {
                // convert date to timestamp in seconds
                var timestamp = value.getTime() / 1e3 >> 0;
                buf.writeIntBE(timestamp, offset, len);
                return offset += len;
            },
            Array: function () {
                return offset;
            }
        };
        if (!type) {
            return offset;
        }
        if (type in ValueSerializerMap) {
            return ValueSerializerMap[type]();
        }
        return offset;
    }
    function writeRecordWithSchema(obj, buf, offset, key, schema) {
        var res; // TODO check res
        if (schema) {
            res = schema.resources[key];
        }
        var val = obj[key];
        var len;
        if (res && Array.isArray(res.type)) {
            var tmp_1 = buffer_1.Buffer.alloc(1024);
            var arrLen_1 = 0;
            val.forEach(function (elem, i) {
                len = length(elem);
                arrLen_1 = writeHeader(tmp_1, arrLen_1, IdentifierType.RESOURCE_INSTANCE, i, len);
                arrLen_1 = writeData(tmp_1, arrLen_1, len, elem, res.type[0]);
            });
            offset = writeHeader(buf, offset, IdentifierType.MULTIPLE_RESOURCE, parseInt(res.id), arrLen_1);
            tmp_1.copy(buf, offset, 0, arrLen_1);
            offset += arrLen_1;
        }
        else {
            len = length(val);
            offset = writeHeader(buf, offset, IdentifierType.RESOURCE_VALUE, parseInt(res.id), len);
            offset = writeData(buf, offset, len, val, res.type);
        }
        return offset;
    }
    /*
     * length in bytes of a number
     */
    function byteLength(val) {
        if (val < 2) {
            return 1;
        }
        return Math.ceil(Math.log(val) * Math.LOG2E / 8); // TODO can be shorter
    }
    function length(val) {
        // integer size: 1, 2, 4 or 8 bytes.
        function size(val) {
            var v = byteLength(val);
            v--;
            v |= v >>> 1;
            v |= v >>> 2;
            v++;
            return v;
        }
        var type = Object.prototype.toString.call(val).slice(8, -1);
        switch (type) {
            case 'Number':
                return (val % 1 === 0 ? size(val) : 8);
            case 'String':
            case 'Uint8Array':
            case 'Buffer':
                return val.length;
            case 'Boolean':
                return 1;
            case 'Date':
                return size(val.getTime() / 1e3 >> 0);
            default:
                return 0;
        }
    }
})(TLV = exports.TLV || (exports.TLV = {}));
