"use strict";
var protobuf = require("../light");

var Type    = protobuf.Type,
    Enum    = protobuf.Enum,
    Field   = protobuf.Field,
    Reader  = protobuf.Reader,
    types   = protobuf.types,
    util    = protobuf.util;

var textformat = protobuf.textformat = module.exports = {};

/**
 * Maximum recursion depth for formatting length-delimited unknown fields.
 * @name unknownRecursionLimit
 * @type {number}
 */
textformat.unknownRecursionLimit = 10;

var punct = {
    "{": true,
    "}": true,
    "<": true,
    ">": true,
    ":": true,
    "[": true,
    "]": true,
    ",": true,
    ";": true,
    "-": true
};

var identRe = /^[A-Za-z_][A-Za-z0-9_]*$/,
    numberStartRe = /[0-9]/,
    wordCharRe = /[A-Za-z0-9_]/,
    hexRe = /^[0-9A-Fa-f]$/,
    octRe = /^[0-7]$/,
    floatRe = /^(?:[0-9]+\.[0-9]*(?:[eE][+-]?[0-9]+)?[fF]?|\.[0-9]+(?:[eE][+-]?[0-9]+)?[fF]?|[0-9]+[eE][+-]?[0-9]+[fF]?|[0-9]+[fF])/,
    hexIntRe = /^0[xX][0-9A-Fa-f]+/,
    octIntRe = /^0[0-7]+/,
    decIntRe = /^(?:0|[1-9][0-9]*)/;

/**
 * Parses protobuf text format using the specified reflected message type.
 * @param {Type} type Reflected message type
 * @param {string} text Text format input
 * @returns {Message<{}>} Message instance
 * @private
 */
function parseText(type, text) {
    if (!(type instanceof Type))
        throw TypeError("type must be a Type");
    type.root.resolveAll();
    var parser = new Parser(String(text));
    var object = parser.parseMessage(type, null, 0);
    parser.expectEnd();
    var err = verifyTextMessage(type, object),
        message;
    if (err)
        throw Error(err);
    message = type.create(object);
    return message;
}

/**
 * Text format options.
 * @interface ITextFormatOptions
 * @property {boolean} [unknowns=false] Also includes and formats unknown fields.
 */

/**
 * Formats a message as protobuf text format using the specified reflected type.
 * @param {Type} type Reflected message type
 * @param {Message<{}>|Object.<string,*>} message Message instance or plain object
 * @param {ITextFormatOptions} [options] Text format options
 * @returns {string} Text format output
 * @private
 */
function formatText(type, message, options) {
    if (!(type instanceof Type))
        throw TypeError("type must be a Type");
    type.root.resolveAll();
    var lines = [];
    writeMessage(type, message, lines, 0, options || {}, 0);
    return lines.join("\n");
}

/**
 * Parses this type from protobuf text format.
 * @param {string} text Text format input
 * @returns {Message<{}>} Message instance
 */
Type.prototype.fromText = function fromText(text) {
    return parseText(this, text);
};

/**
 * Formats a message of this type as protobuf text format.
 * @param {Message<{}>|Object.<string,*>} message Message instance or plain object
 * @param {ITextFormatOptions} [options] Text format options
 * @returns {string} Text format output
 */
Type.prototype.toText = function toText(message, options) {
    return formatText(this, message, options);
};

function Tokenizer(source) {
    this.source = source;
    this.pos = 0;
    this.len = source.length;
    this.line = 1;
    this.stack = [];
}

Tokenizer.prototype.error = function error(message) {
    return Error(message + " (line " + this.line + ")");
};

Tokenizer.prototype.peek = function peek() {
    var token = this.next();
    this.push(token);
    return token;
};

Tokenizer.prototype.push = function push(token) {
    if (token)
        this.stack.unshift(token);
};

Tokenizer.prototype.next = function next() {
    if (this.stack.length)
        return this.stack.shift();
    this.skipSpace();
    if (this.pos >= this.len)
        return null;

    var ch = this.source.charAt(this.pos);
    if (ch === "\"" || ch === "'")
        return this.readString(ch);
    if (ch === "." && numberStartRe.test(this.source.charAt(this.pos + 1)) || numberStartRe.test(ch))
        return this.readNumber();
    if (punct[ch]) {
        ++this.pos;
        return { type: "punct", value: ch, raw: ch, line: this.line };
    }
    return this.readWord();
};

Tokenizer.prototype.expect = function expect(value) {
    var token = this.next();
    if (!token || token.value !== value)
        throw this.error("expected '" + value + "'");
    return token;
};

Tokenizer.prototype.skip = function skip(value) {
    var token = this.next();
    if (token && token.value === value)
        return true;
    this.push(token);
    return false;
};

Tokenizer.prototype.skipSpace = function skipSpace() {
    var ch;
    while (this.pos < this.len) {
        ch = this.source.charAt(this.pos);
        if (ch === "#") {
            while (this.pos < this.len && this.source.charAt(this.pos) !== "\n")
                ++this.pos;
        } else if (ch === " " || ch === "\t" || ch === "\v" || ch === "\f" || ch === "\r") {
            ++this.pos;
        } else if (ch === "\n") {
            ++this.pos;
            ++this.line;
        } else
            break;
    }
};

Tokenizer.prototype.readWord = function readWord() {
    var start = this.pos,
        ch;
    while (this.pos < this.len) {
        ch = this.source.charAt(this.pos);
        if (ch === "#" || ch === "\"" || ch === "'" || punct[ch] || /\s/.test(ch))
            break;
        ++this.pos;
    }
    if (start === this.pos)
        throw this.error("illegal character '" + this.source.charAt(this.pos) + "'");
    var raw = this.source.substring(start, this.pos);
    return { type: "word", value: raw, raw: raw, line: this.line };
};

Tokenizer.prototype.readNumber = function readNumber() {
    var rest = this.source.substring(this.pos),
        matches = [
            floatRe.exec(rest),
            hexIntRe.exec(rest),
            octIntRe.exec(rest),
            decIntRe.exec(rest)
        ],
        raw = null;
    for (var i = 0; i < matches.length; ++i)
        if (matches[i] && (!raw || matches[i][0].length > raw.length))
            raw = matches[i][0];
    if (!raw)
        throw this.error("illegal number");
    this.pos += raw.length;
    if (this.pos < this.len && wordCharRe.test(this.source.charAt(this.pos)))
        throw this.error("illegal number '" + raw + this.source.charAt(this.pos) + "'");
    return { type: "number", value: raw, raw: raw, line: this.line };
};

Tokenizer.prototype.readString = function readString(quote) {
    var bytes = [],
        ch,
        code;
    ++this.pos;
    while (this.pos < this.len) {
        ch = this.source.charAt(this.pos++);
        if (ch === quote)
            return { type: "string", value: bytes, raw: quote, line: this.line };
        if (ch === "\n")
            throw this.error("illegal string");
        if (ch !== "\\") {
            code = ch.charCodeAt(0);
            if ((code & 0xFC00) === 0xD800 && this.pos < this.len) {
                var next = this.source.charCodeAt(this.pos);
                if ((next & 0xFC00) === 0xDC00) {
                    ++this.pos;
                    code = 0x10000 + ((code & 0x03FF) << 10) + (next & 0x03FF);
                }
            }
            pushUtf8(bytes, code);
            continue;
        }
        if (this.pos >= this.len)
            throw this.error("illegal string escape");
        ch = this.source.charAt(this.pos++);
        switch (ch) {
            case "a": bytes.push(7); break;
            case "b": bytes.push(8); break;
            case "f": bytes.push(12); break;
            case "n": bytes.push(10); break;
            case "r": bytes.push(13); break;
            case "t": bytes.push(9); break;
            case "v": bytes.push(11); break;
            case "?": bytes.push(63); break;
            case "\\": bytes.push(92); break;
            case "'": bytes.push(39); break;
            case "\"": bytes.push(34); break;
            case "x":
            case "X":
                bytes.push(this.readHexEscape());
                break;
            case "u":
                pushUtf8(bytes, this.readCodePoint(4));
                break;
            case "U":
                pushUtf8(bytes, this.readCodePoint(8));
                break;
            default:
                if (octRe.test(ch)) {
                    bytes.push(this.readOctEscape(ch));
                    break;
                }
                throw this.error("illegal string escape '\\" + ch + "'");
        }
    }
    throw this.error("unterminated string");
};

Tokenizer.prototype.readHexEscape = function readHexEscape() {
    var value = 0,
        count = 0,
        ch;
    while (count < 2 && this.pos < this.len && hexRe.test(ch = this.source.charAt(this.pos))) {
        value = value * 16 + parseInt(ch, 16);
        ++this.pos;
        ++count;
    }
    if (!count)
        throw this.error("illegal hex escape");
    return value;
};

Tokenizer.prototype.readOctEscape = function readOctEscape(first) {
    var value = parseInt(first, 8),
        count = 1,
        ch;
    while (count < 3 && this.pos < this.len && octRe.test(ch = this.source.charAt(this.pos))) {
        value = value * 8 + parseInt(ch, 8);
        ++this.pos;
        ++count;
    }
    return value & 255;
};

Tokenizer.prototype.readCodePoint = function readCodePoint(size) {
    var raw = this.source.substring(this.pos, this.pos + size);
    if (raw.length !== size || !/^[0-9A-Fa-f]+$/.test(raw))
        throw this.error("illegal unicode escape");
    this.pos += size;
    var code = parseInt(raw, 16);
    if (code > 0x10FFFF || code >= 0xD800 && code <= 0xDFFF)
        throw this.error("illegal unicode escape");
    return code;
};

function Parser(source) {
    this.tn = new Tokenizer(source);
}

Parser.prototype.error = function error(message) {
    throw this.tn.error(message);
};

Parser.prototype.expectEnd = function expectEnd() {
    var token = this.tn.next();
    if (token)
        this.error("unexpected token '" + token.value + "'");
};

Parser.prototype.parseMessage = function parseMessage(type, end, depth) {
    if (depth > util.recursionLimit)
        this.error("max depth exceeded");
    var object = {},
        seen = {};
    for (;;) {
        var token = this.tn.peek();
        if (!token) {
            if (end)
                this.error("expected '" + end + "'");
            return object;
        }
        if (end && token.value === end) {
            this.tn.next();
            return object;
        }
        if (isSeparator(token))
            this.error("unexpected separator");
        this.parseField(type, object, seen, depth);
        token = this.tn.peek();
        if (token && isSeparator(token)) {
            this.tn.next();
            token = this.tn.peek();
            if (token && isSeparator(token))
                this.error("unexpected separator");
        }
    }
};

Parser.prototype.parseField = function parseField(type, object, seen, depth) {
    var info = this.readFieldName(type);
    if (info.any) {
        this.parseAny(type, info.name, object, seen, depth);
        return;
    }
    if (!info.field) {
        this.skipReservedValue(depth);
        return;
    }

    var field = info.field.resolve();
    if (field.map) {
        this.parseMessageFieldDelimiter();
        this.parseMapField(field, object, depth);
    } else if (field.resolvedType instanceof Type) {
        this.parseMessageFieldDelimiter();
        if (this.tn.skip("[")) {
            if (!field.repeated)
                this.error(field.fullName + ": list value specified for singular field");
            if (!this.tn.skip("]")) {
                do {
                    addField(object, seen, field, this.parseMessageValue(field.resolvedType, depth));
                } while (this.tn.skip(","));
                this.tn.expect("]");
            }
        } else
            addField(object, seen, field, this.parseMessageValue(field.resolvedType, depth));
    } else {
        this.tn.expect(":");
        if (this.tn.skip("[")) {
            if (!field.repeated)
                this.error(field.fullName + ": list value specified for singular field");
            if (!this.tn.skip("]")) {
                do {
                    addField(object, seen, field, this.parseScalar(field));
                } while (this.tn.skip(","));
                this.tn.expect("]");
            }
        } else
            addField(object, seen, field, this.parseScalar(field));
    }
};

Parser.prototype.readFieldName = function readFieldName(type) {
    var token = this.tn.next();
    if (!token)
        this.error("expected field name");
    if (token.value === "[") {
        var bracketName = this.readBracketName();
        if (type.fullName === ".google.protobuf.Any" && bracketName.indexOf("/") >= 0)
            return { any: true, name: bracketName };
        return { field: lookupExtension(type, bracketName), name: bracketName };
    }
    if (token.type !== "word" || !identRe.test(token.value))
        this.error("illegal field name '" + token.value + "'");

    var field = lookupField(type, token.value);
    if (!field) {
        if (type.isReservedName(token.value))
            return { name: token.value };
        this.error(type.fullName + ": unknown field '" + token.value + "'");
    }
    return { field: field, name: token.value };
};

Parser.prototype.readBracketName = function readBracketName() {
    var parts = [],
        token;
    while (token = this.tn.next()) {
        if (token.value === "]")
            return parts.join("");
        parts.push(token.raw || token.value);
    }
    this.error("unterminated bracketed field name");
    return null;
};

Parser.prototype.parseAny = function parseAny(type, typeUrl, object, seen, depth) {
    this.parseMessageFieldDelimiter();
    validateTypeUrl(typeUrl);
    var typeName = typeUrl.substring(typeUrl.lastIndexOf("/") + 1),
        embeddedType = type.root.lookupType(typeName.charAt(0) === "." ? typeName : "." + typeName),
        embedded = this.parseMessageValue(embeddedType, depth),
        typeUrlField = lookupField(type, "type_url") || lookupField(type, "typeUrl"),
        valueField = lookupField(type, "value");
    addField(object, seen, typeUrlField, typeUrl);
    addField(object, seen, valueField, embeddedType.encode(embedded).finish());
};

Parser.prototype.parseMapField = function parseMapField(field, object, depth) {
    if (!object[field.name])
        object[field.name] = {};
    if (this.tn.skip("[")) {
        if (!this.tn.skip("]")) {
            do {
                addMapEntry(field, object[field.name], this.parseMapEntry(field, depth + 1));
            } while (this.tn.skip(","));
            this.tn.expect("]");
        }
    } else
        addMapEntry(field, object[field.name], this.parseMapEntry(field, depth + 1));
};

Parser.prototype.parseMessageFieldDelimiter = function parseMessageFieldDelimiter() {
    this.tn.skip(":");
};

Parser.prototype.parseMapEntry = function parseMapEntry(field, depth) {
    if (depth > util.recursionLimit)
        this.error("max depth exceeded");
    var end;
    if (this.tn.skip("{"))
        end = "}";
    else if (this.tn.skip("<"))
        end = ">";
    else
        this.error("expected map entry");

    var entry = {},
        token,
        valueField = mapValueField(field),
        keyField = mapKeyField(field);
    for (;;) {
        token = this.tn.peek();
        if (!token)
            this.error("expected '" + end + "'");
        if (token.value === end) {
            this.tn.next();
            return entry;
        }
        if (isSeparator(token))
            this.error("unexpected separator");
        token = this.tn.next();
        if (token.type !== "word")
            this.error("expected map field name");
        if (token.value === "key") {
            this.tn.expect(":");
            entry.key = this.parseScalar(keyField);
        } else if (token.value === "value") {
            if (valueField.resolvedType instanceof Type) {
                this.parseMessageFieldDelimiter();
                entry.value = this.parseMessageValue(valueField.resolvedType, depth);
            } else {
                this.tn.expect(":");
                entry.value = this.parseScalar(valueField);
            }
        } else
            this.error("unknown map field '" + token.value + "'");
        token = this.tn.peek();
        if (token && isSeparator(token)) {
            this.tn.next();
            token = this.tn.peek();
            if (token && isSeparator(token))
                this.error("unexpected separator");
        }
    }
};

Parser.prototype.parseMessageValue = function parseMessageValue(type, depth) {
    var end;
    if (this.tn.skip("{"))
        end = "}";
    else if (this.tn.skip("<"))
        end = ">";
    else
        this.error("expected message value");
    return this.parseMessage(type, end, depth + 1);
};

Parser.prototype.parseScalar = function parseScalar(field) {
    if (field.type === "string" || field.type === "bytes") {
        var bytes = this.readStringBytes();
        return field.type === "bytes" ? util.newBuffer(bytes) : utf8Read(bytes, true);
    }

    var sign = 1;
    if (this.tn.skip("-"))
        sign = -1;

    var token = this.tn.next();
    if (!token)
        this.error("expected value");

    if (field.resolvedType instanceof Enum)
        return parseEnum(field, token, sign);

    switch (field.type) {
        case "double":
        case "float":
            return parseFloatValue(token, sign);
        case "bool":
            return parseBool(token, sign);
        case "int32":
        case "sint32":
        case "sfixed32":
            return parseInteger(token, sign, false, 32);
        case "uint32":
        case "fixed32":
            return parseInteger(token, sign, true, 32);
        case "int64":
        case "sint64":
        case "sfixed64":
            return parseInteger(token, sign, false, 64);
        case "uint64":
        case "fixed64":
            return parseInteger(token, sign, true, 64);
        default:
            this.error(field.fullName + ": unsupported scalar type");
    }
    return undefined;
};

Parser.prototype.readStringBytes = function readStringBytes() {
    var token = this.tn.next(),
        bytes = [];
    if (!token || token.type !== "string")
        this.error("expected string");
    Array.prototype.push.apply(bytes, token.value);
    while ((token = this.tn.peek()) && token.type === "string") {
        token = this.tn.next();
        Array.prototype.push.apply(bytes, token.value);
    }
    return bytes;
};

Parser.prototype.skipReservedValue = function skipReservedValue(depth) {
    this.tn.skip(":");
    var token = this.tn.peek();
    if (!token)
        return;
    if (token.value === "{") {
        this.skipBalanced("{", "}", depth);
    } else if (token.value === "<") {
        this.skipBalanced("<", ">", depth);
    } else if (token.value === "[") {
        this.skipBalanced("[", "]", depth);
    } else if (token.type === "string") {
        this.readStringBytes();
    } else {
        if (token.value === "-")
            this.tn.next();
        this.tn.next();
    }
};

Parser.prototype.skipBalanced = function skipBalanced(open, close, depth) {
    var balance = 0,
        token;
    do {
        token = this.tn.next();
        if (!token)
            this.error("expected '" + close + "'");
        if (token.value === open) {
            if (depth + ++balance > util.recursionLimit)
                this.error("max depth exceeded");
        }
        else if (token.value === close)
            --balance;
    } while (balance);
};

function addField(object, seen, field, value) {
    if (field.repeated) {
        (object[field.name] || (object[field.name] = [])).push(value);
        return;
    }
    if (Object.prototype.hasOwnProperty.call(seen, field.name))
        throw Error(field.fullName + ": multiple values");
    if (field.partOf) {
        var oneofName = "$oneof:" + field.partOf.name;
        if (seen[oneofName])
            throw Error(field.partOf.name + ": multiple values");
        seen[oneofName] = true;
    }
    seen[field.name] = true;
    object[field.name] = value;
}

function addMapEntry(field, map, entry) {
    var keyField = mapKeyField(field),
        valueField = mapValueField(field),
        key = Object.prototype.hasOwnProperty.call(entry, "key")
            ? entry.key
            : defaultScalarValue(keyField),
        value = Object.prototype.hasOwnProperty.call(entry, "value")
            ? entry.value
            : defaultMapValue(valueField);
    map[mapKey(keyField, key)] = value;
}

function defaultMapValue(field) {
    if (field.resolvedType instanceof Type)
        return field.resolvedType.create();
    if (field.resolvedType instanceof Enum)
        return field.typeDefault;
    return defaultScalarValue(field);
}

function defaultScalarValue(field) {
    if (field.type === "string")
        return "";
    if (field.type === "bytes")
        return util.newBuffer([]);
    if (field.type === "bool")
        return false;
    return field.defaultValue;
}

function mapKeyField(field) {
    return {
        name: "key",
        fullName: field.fullName + ".key",
        type: field.keyType,
        resolvedType: null,
        defaultValue: field.keyType === "bool" ? false : field.keyType === "string" ? "" : 0
    };
}

function mapValueField(field) {
    return {
        name: "value",
        fullName: field.fullName + ".value",
        type: field.type,
        resolvedType: field.resolvedType,
        defaultValue: field.resolvedType instanceof Enum ? field.resolvedType.values[Object.keys(field.resolvedType.values)[0]] : field.typeDefault
    };
}

function mapKey(field, value) {
    if (types.long[field.type] !== undefined)
        return String(value);
    if (field.type === "bool")
        return value ? "true" : "false";
    return String(value);
}

function lookupField(type, name) {
    var fields = type.fieldsArray;
    for (var i = 0; i < fields.length; ++i) {
        var field = fields[i].resolve();
        if (field.name === name || underScore(field.name) === name)
            return field;
        if (field.delimited && field.resolvedType instanceof Type) {
            var groupName = field.resolvedType.name,
                lowerName = name.toLowerCase(),
                compactName = compact(name);
            if (groupName === name || underScore(groupName) === name
             || field.name.toLowerCase() === lowerName
             || groupName.toLowerCase() === lowerName
             || compact(field.name) === compactName
             || compact(groupName) === compactName)
                return field;
        }
    }
    return null;
}

function lookupExtension(type, name) {
    var fullName = name.charAt(0) === "." ? name : "." + name,
        camelName = camelCaseLastPath(fullName),
        field = type.get(fullName) || type.root.lookup(fullName, Field)
             || type.get(camelName) || type.root.lookup(camelName, Field);
    if (!field)
        field = lookupExtensionField(type, fullName);
    if (field && field.extend !== undefined && field.extensionField)
        field = field.extensionField;
    if (!field || field.parent !== type)
        throw Error(type.fullName + ": unknown extension '" + name + "'");
    return field;
}

function camelCaseLastPath(name) {
    var dot = name.lastIndexOf(".");
    return name.substring(0, dot + 1) + util.camelCase(name.substring(dot + 1));
}

function parseEnum(field, token, sign) {
    if (sign < 0) {
        if (token.type !== "number")
            throw Error(field.fullName + ": enum value expected");
        return checkEnumValue(field, parseInteger(token, sign, false, 32));
    }
    if (token.type === "word") {
        var value = field.resolvedType.values[token.value];
        if (value === undefined)
            throw Error(field.fullName + ": enum value expected");
        return value;
    }
    return checkEnumValue(field, parseInteger(token, sign, false, 32));
}

function parseBool(token, sign) {
    if (sign < 0)
        throw Error("bool value expected");
    if (token.type === "word") {
        switch (token.value) {
            case "true":
            case "True":
            case "t":
                return true;
            case "false":
            case "False":
            case "f":
                return false;
        }
    } else if (token.type === "number") {
        var value = parseInteger(token, 1, true, 32);
        if (value === 0)
            return false;
        if (value === 1)
            return true;
    }
    throw Error("bool value expected");
}

function parseFloatValue(token, sign) {
    if (token.type === "word") {
        switch (token.value.toLowerCase()) {
            case "inf":
            case "infinity":
                return sign * Infinity;
            case "nan":
                return NaN;
        }
        throw Error("float value expected");
    }
    if (token.type !== "number")
        throw Error("float value expected");
    var raw = token.value;
    if (/^0[xX]/.test(raw) || /^0[0-7]/.test(raw) && raw.length > 1)
        throw Error("float value expected");
    if (/[fF]$/.test(raw))
        raw = raw.substring(0, raw.length - 1);
    return sign * Number(raw);
}

function parseInteger(token, sign, unsigned, bits) {
    if (token.type !== "number")
        throw Error("integer value expected");
    if (sign < 0 && unsigned)
        throw Error("unsigned integer value expected");

    var raw = token.value,
        radix = 10,
        digits = raw;
    if (/^0[xX]/.test(raw)) {
        radix = 16;
        digits = raw.substring(2);
    } else if (/^0[0-7]/.test(raw) && raw.length > 1) {
        radix = 8;
        digits = raw.substring(1);
    } else if (!/^(?:0|[1-9][0-9]*)$/.test(raw))
        throw Error("integer value expected");

    if (typeof util.global.BigInt === "function")
        return parseIntegerBigInt(digits, radix, sign, unsigned, bits);

    var value = parseInt(digits, radix) * sign;
    if (bits === 64 && util.Long)
        return util.Long.fromString(String(value), unsigned);
    return value;
}

function parseIntegerBigInt(digits, radix, sign, unsigned, bits) {
    var value,
        BigInt = util.global.BigInt;
    if (radix === 16)
        value = BigInt("0x" + digits);
    else if (radix === 8)
        value = BigInt("0o" + digits);
    else
        value = BigInt(digits);
    if (sign < 0)
        value = -value;

    var min,
        max;
    if (bits === 32) {
        min = unsigned ? BigInt(0) : -BigInt(2147483648);
        max = unsigned ? BigInt(4294967295) : BigInt(2147483647);
    } else {
        min = unsigned ? BigInt(0) : -BigInt("9223372036854775808");
        max = unsigned ? BigInt("18446744073709551615") : BigInt("9223372036854775807");
    }
    if (value < min || value > max)
        throw Error((unsigned ? "unsigned " : "") + "integer value out of range");
    if (bits === 64 && util.Long)
        return util.Long.fromString(value.toString(), unsigned, 10);
    return Number(value);
}

function writeMessage(type, message, lines, indent, options, depth) {
    if (depth > util.recursionLimit)
        throw Error("max depth exceeded");
    var fields = type.fieldsArray.slice().sort(util.compareFieldsById);
    for (var i = 0; i < fields.length; ++i) {
        var field = fields[i].resolve(),
            value = message[field.name];
        if (value == null || !Object.prototype.hasOwnProperty.call(message, field.name))
            continue;
        if (field.map)
            writeMapField(field, value, lines, indent, options, depth);
        else if (field.repeated)
            for (var j = 0; j < value.length; ++j)
                writeField(field, value[j], lines, indent, options, depth);
        else
            writeField(field, value, lines, indent, options, depth);
    }
    if (options.unknowns && message.$unknowns != null && Object.prototype.hasOwnProperty.call(message, "$unknowns"))
        writeUnknowns(message.$unknowns, lines, indent);
}

function writeMapField(field, map, lines, indent, options, depth) {
    var keys = Object.keys(map).sort(),
        keyField = mapKeyField(field),
        valueField = mapValueField(field),
        name = formatFieldName(field),
        sp = spaces(indent);
    for (var i = 0; i < keys.length; ++i) {
        if (depth + 1 > util.recursionLimit)
            throw Error("max depth exceeded");
        var key = keyField.long ? util.longFromKey(keys[i], keyField.type === "uint64" || keyField.type === "fixed64") : keys[i];
        if (keyField.type === "bool")
            key = util.boolFromKey(keys[i]);
        lines.push(sp + name + " {");
        lines.push(spaces(indent + 2) + "key: " + formatScalar(keyField, key));
        if (valueField.resolvedType instanceof Type) {
            lines.push(spaces(indent + 2) + "value {");
            writeMessage(valueField.resolvedType, map[keys[i]], lines, indent + 4, options, depth + 2);
            lines.push(spaces(indent + 2) + "}");
        } else
            lines.push(spaces(indent + 2) + "value: " + formatScalar(valueField, map[keys[i]]));
        lines.push(sp + "}");
    }
}

function writeField(field, value, lines, indent, options, depth) {
    var name = formatFieldName(field),
        sp = spaces(indent);
    if (field.resolvedType instanceof Type) {
        lines.push(sp + name + " {");
        writeMessage(field.resolvedType, value, lines, indent + 2, options, depth + 1);
        lines.push(sp + "}");
    } else
        lines.push(sp + name + ": " + formatScalar(field, value));
}

function writeUnknowns(unknowns, lines, indent) {
    for (var i = 0; i < unknowns.length; ++i) {
        var reader = Reader.create(unknowns[i]);
        writeUnknownFieldSet(reader, lines, indent, undefined, textformat.unknownRecursionLimit);
        if (reader.pos !== reader.len)
            throw Error("invalid unknown field data");
    }
}

function writeUnknownFieldSet(reader, lines, indent, endGroup, recursionBudget) {
    if (recursionBudget < 0)
        throw Error("max depth exceeded");
    while (reader.pos < reader.len) {
        var tag = reader.tag(),
            fieldNumber = tag >>> 3,
            wireType = tag & 7;
        if (!fieldNumber)
            throw Error("illegal tag: field number 0");
        if (wireType === 4) {
            if (fieldNumber !== endGroup)
                throw Error("invalid end group tag");
            return;
        }
        writeUnknownField(reader, fieldNumber, wireType, lines, indent, recursionBudget);
    }
    if (endGroup !== undefined)
        throw Error("missing end group tag");
}

function writeUnknownField(reader, fieldNumber, wireType, lines, indent, recursionBudget) {
    var sp = spaces(indent),
        lo;
    switch (wireType) {
        case 0:
            lines.push(sp + fieldNumber + ": " + readUnknownVarint(reader));
            break;
        case 1:
            lo = reader.fixed32();
            lines.push(sp + fieldNumber + ": 0x" + hexPad(reader.fixed32(), 8) + hexPad(lo, 8));
            break;
        case 2:
            writeUnknownLengthDelimited(reader, fieldNumber, lines, indent, recursionBudget);
            break;
        case 3:
            lines.push(sp + fieldNumber + " {");
            writeUnknownFieldSet(reader, lines, indent + 2, fieldNumber, recursionBudget - 1);
            lines.push(sp + "}");
            break;
        case 5:
            lines.push(sp + fieldNumber + ": 0x" + hexPad(reader.fixed32(), 8));
            break;
        default:
            throw Error("invalid wire type " + wireType);
    }
}

function writeUnknownLengthDelimited(reader, fieldNumber, lines, indent, recursionBudget) {
    var value = reader.bytes(),
        nested = value.length && recursionBudget > 0
            ? tryFormatUnknownMessage(value, indent + 2, recursionBudget - 1)
            : null,
        sp = spaces(indent);
    if (nested) {
        lines.push(sp + fieldNumber + " {");
        for (var i = 0; i < nested.length; ++i)
            lines.push(nested[i]);
        lines.push(sp + "}");
    } else
        lines.push(sp + fieldNumber + ": " + quoteBytes(value));
}

function tryFormatUnknownMessage(bytes, indent, recursionBudget) {
    var reader = Reader.create(bytes),
        lines = [];
    try {
        writeUnknownFieldSet(reader, lines, indent, undefined, recursionBudget);
        return reader.pos === reader.len ? lines : null;
    } catch (e) {
        return null;
    }
}

function readUnknownVarint(reader) {
    var BigInt = util.global.BigInt;
    if (typeof BigInt !== "function")
        return String(reader.uint64());

    var value = BigInt(0),
        shift = BigInt(0),
        b;
    for (var i = 0; i < 10; ++i) {
        if (reader.pos >= reader.len)
            throw Error("invalid varint encoding");
        b = reader.buf[reader.pos++];
        value += BigInt(b & 127) << shift;
        if (b < 128)
            return value.toString();
        shift += BigInt(7);
    }
    throw Error("invalid varint encoding");
}

function hexPad(value, length) {
    var str = value.toString(16);
    while (str.length < length)
        str = "0" + str;
    return str;
}

function formatFieldName(field) {
    if (field.declaringField || field.name.charAt(0) === ".")
        return "[" + formatExtensionName(field) + "]";
    if (field.delimited && field.resolvedType instanceof Type && compact(field.name) === compact(field.resolvedType.name))
        return field.resolvedType.name;
    return underScore(field.name);
}

function formatExtensionName(field) {
    var name = field.name.replace(/^\./, ""),
        dot = name.lastIndexOf("."),
        path = name.substring(0, dot + 1),
        last = name.substring(dot + 1);
    if (field.delimited && field.resolvedType instanceof Type && field.resolvedType.group)
        last = last.toLowerCase();
    else
        last = underScore(last);
    return path + last;
}

function formatScalar(field, value) {
    if (field.resolvedType instanceof Enum) {
        var name = field.resolvedType.valuesById[value];
        return name === undefined ? String(value) : name;
    }
    switch (field.type) {
        case "string":
            return quoteBytes(utf8Bytes(String(value)));
        case "bytes":
            return quoteBytes(bytesValue(value));
        case "bool":
            return value ? "true" : "false";
        case "double":
        case "float":
            if (isNaN(value))
                return "nan";
            if (value === 0 && 1 / value === -Infinity)
                return "-0";
            if (value === Infinity)
                return "inf";
            if (value === -Infinity)
                return "-inf";
            return String(value);
        default:
            return String(value);
    }
}

function bytesValue(value) {
    if (typeof value === "string") {
        var length = util.base64.length(value),
            buffer = util.newBuffer(length);
        util.base64.decode(value, buffer, 0);
        return buffer;
    }
    return value;
}

function quoteBytes(bytes) {
    var out = "\"",
        oct;
    for (var i = 0; i < bytes.length; ++i) {
        var b = bytes[i];
        switch (b) {
            case 9: out += "\\t"; break;
            case 10: out += "\\n"; break;
            case 13: out += "\\r"; break;
            case 34: out += "\\\""; break;
            case 92: out += "\\\\"; break;
            default:
                if (b >= 32 && b <= 126)
                    out += String.fromCharCode(b);
                else {
                    oct = b.toString(8);
                    out += "\\" + (oct.length < 2 ? "00" : oct.length < 3 ? "0" : "") + oct;
                }
                break;
        }
    }
    return out + "\"";
}

function spaces(indent) {
    var str = "";
    while (str.length < indent)
        str += " ";
    return str;
}

function utf8Read(bytes, strict) {
    if (strict && !validUtf8(bytes))
        throw Error("invalid UTF-8 string");
    var buffer = util.newBuffer(bytes);
    return util.utf8.read(buffer, 0, buffer.length);
}

function validUtf8(bytes) {
    for (var i = 0, b; i < bytes.length;) {
        b = bytes[i++];
        if (b <= 0x7F)
            continue;
        if (b >= 0xC2 && b <= 0xDF) {
            if ((b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b === 0xE0) {
            if ((b = bytes[i++]) < 0xA0 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b >= 0xE1 && b <= 0xEC || b >= 0xEE && b <= 0xEF) {
            if ((b = bytes[i++]) < 0x80 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b === 0xED) {
            if ((b = bytes[i++]) < 0x80 || b > 0x9F || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b === 0xF0) {
            if ((b = bytes[i++]) < 0x90 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b >= 0xF1 && b <= 0xF3) {
            if ((b = bytes[i++]) < 0x80 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else if (b === 0xF4) {
            if ((b = bytes[i++]) < 0x80 || b > 0x8F || (b = bytes[i++]) < 0x80 || b > 0xBF || (b = bytes[i++]) < 0x80 || b > 0xBF)
                return false;
        } else
            return false;
    }
    return true;
}

function utf8Bytes(str) {
    var buffer = util.newBuffer(util.utf8.length(str));
    util.utf8.write(str, buffer, 0);
    return buffer;
}

function pushUtf8(bytes, code) {
    if (code < 0x80) {
        bytes.push(code);
    } else if (code < 0x800) {
        bytes.push(code >> 6 | 192, code & 63 | 128);
    } else if (code < 0x10000) {
        bytes.push(code >> 12 | 224, code >> 6 & 63 | 128, code & 63 | 128);
    } else {
        bytes.push(code >> 18 | 240, code >> 12 & 63 | 128, code >> 6 & 63 | 128, code & 63 | 128);
    }
}

function verifyTextMessage(type, message) {
    var fields = type.fieldsArray;
    for (var i = 0; i < fields.length; ++i) {
        var field = fields[i].resolve(),
            value = message[field.name];
        if (value == null || !Object.prototype.hasOwnProperty.call(message, field.name)) {
            if (field.required)
                return field.fullName + ": missing required field";
            continue;
        }
        if (field.map) {
            if (field.resolvedType instanceof Type)
                for (var key in value)
                    if (Object.prototype.hasOwnProperty.call(value, key)) {
                        var mapErr = verifyTextMessage(field.resolvedType, value[key]);
                        if (mapErr)
                            return mapErr;
                    }
        } else if (field.repeated) {
            if (field.resolvedType instanceof Type)
                for (var j = 0; j < value.length; ++j) {
                    var repeatedErr = verifyTextMessage(field.resolvedType, value[j]);
                    if (repeatedErr)
                        return repeatedErr;
                }
        } else if (field.resolvedType instanceof Type) {
            var err = verifyTextMessage(field.resolvedType, value);
            if (err)
                return err;
        }
    }
    return null;
}

function isSeparator(token) {
    return token.value === "," || token.value === ";";
}

function validateTypeUrl(typeUrl) {
    for (var i = 0; i < typeUrl.length; ++i)
        if (typeUrl.charAt(i) === "%") {
            if (i + 2 >= typeUrl.length || !hexRe.test(typeUrl.charAt(i + 1)) || !hexRe.test(typeUrl.charAt(i + 2)))
                throw Error("invalid Any type URL");
            i += 2;
        }
}

function lookupExtensionField(type, fullName) {
    var fields = type.fieldsArray;
    for (var i = 0; i < fields.length; ++i) {
        var field = fields[i].resolve();
        if (field.name.charAt(0) === "." && "." + formatExtensionName(field) === fullName)
            return field;
    }
    return null;
}

function checkEnumValue(field, value) {
    if (isClosedEnum(field) && field.resolvedType.valuesById[value] === undefined)
        throw Error(field.fullName + ": enum value expected");
    return value;
}

function isClosedEnum(field) {
    return field.resolvedType
        && field.resolvedType._features
        && field.resolvedType._features.enum_type === "CLOSED";
}

function compact(str) {
    return str.replace(/_/g, "").toLowerCase();
}

function underScore(str) {
    return str.substring(0, 1)
         + str.substring(1)
               .replace(/([A-Z])(?=[a-z]|$)/g, function($0, $1) { return "_" + $1.toLowerCase(); });
}
