// Copyright 2021-2026 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { ScalarType, } from "../descriptors.js";
/**
 * Create a PathBuilder.
 */
export function buildPath(schema) {
    return new PathBuilderImpl(schema, schema, []);
}
/**
 * Parse a Path from a string.
 *
 * Throws an InvalidPathError if the path is invalid.
 *
 * Note that a Registry must be provided via the options argument to parse
 * paths that refer to an extension.
 */
export function parsePath(schema, path, options) {
    var _a, _b;
    const builder = new PathBuilderImpl(schema, schema, []);
    const err = (message, i) => new InvalidPathError(schema, message + " at column " + (i + 1), path);
    for (let i = 0; i < path.length;) {
        const token = nextToken(i, path);
        const left = builder.getLeft();
        let right = undefined;
        if ("field" in token) {
            right =
                (left === null || left === void 0 ? void 0 : left.kind) != "message"
                    ? undefined
                    : ((_a = left.fields.find((field) => field.name === token.field)) !== null && _a !== void 0 ? _a : left.oneofs.find((oneof) => oneof.name === token.field));
            if (!right) {
                throw err(`Unknown field "${token.field}"`, i);
            }
        }
        else if ("ext" in token) {
            right = (_b = options === null || options === void 0 ? void 0 : options.registry) === null || _b === void 0 ? void 0 : _b.getExtension(token.ext);
            if (!right) {
                throw err(`Unknown extension "${token.ext}"`, i);
            }
        }
        else if ("val" in token) {
            // list or map
            right =
                (left === null || left === void 0 ? void 0 : left.kind) == "field" &&
                    left.fieldKind == "list" &&
                    typeof token.val == "bigint"
                    ? { kind: "list_sub", index: Number(token.val) }
                    : { kind: "map_sub", key: token.val };
        }
        else if ("err" in token) {
            throw err(token.err, token.i);
        }
        if (right) {
            try {
                builder.add([right]);
            }
            catch (e) {
                throw err(e instanceof InvalidPathError ? e.message : String(e), i);
            }
        }
        i = token.i;
    }
    return builder.toPath();
}
/**
 * Stringify a path.
 */
export function pathToString(path) {
    const str = [];
    for (const ele of path) {
        switch (ele.kind) {
            case "field":
            case "oneof":
                if (str.length > 0) {
                    str.push(".");
                }
                str.push(ele.name);
                break;
            case "extension":
                str.push("[", ele.typeName, "]");
                break;
            case "list_sub":
                str.push("[", ele.index, "]");
                break;
            case "map_sub":
                if (typeof ele.key == "string") {
                    str.push('["', ele.key
                        .split("\\")
                        .join("\\\\")
                        .split('"')
                        .join('\\"')
                        .split("\r")
                        .join("\\r")
                        .split("\n")
                        .join("\\n"), '"]');
                }
                else {
                    str.push("[", ele.key, "]");
                }
                break;
        }
    }
    return str.join("");
}
/**
 * InvalidPathError is thrown for invalid Paths, for example during parsing from
 * a string, or when a new Path is built.
 */
export class InvalidPathError extends Error {
    constructor(schema, message, path) {
        super(message);
        this.name = "InvalidPathError";
        this.schema = schema;
        this.path = path;
        // see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#example
        Object.setPrototypeOf(this, new.target.prototype);
    }
}
class PathBuilderImpl {
    constructor(schema, left, path) {
        this.schema = schema;
        this.left = left;
        this.path = path;
    }
    getLeft() {
        return this.left;
    }
    field(field) {
        return this.push(field);
    }
    oneof(oneof) {
        return this.push(oneof);
    }
    extension(extension) {
        return this.push(extension);
    }
    list(index) {
        return this.push({ kind: "list_sub", index });
    }
    map(key) {
        return this.push({ kind: "map_sub", key });
    }
    add(pathOrBuilder) {
        const path = Array.isArray(pathOrBuilder)
            ? pathOrBuilder
            : pathOrBuilder.toPath();
        const l = this.path.length;
        try {
            for (const ele of path) {
                this.push(ele);
            }
        }
        catch (e) {
            // undo pushes
            this.path.splice(l);
            throw e;
        }
        return this;
    }
    toPath() {
        return this.path.concat();
    }
    clone() {
        return new PathBuilderImpl(this.schema, this.left, this.path.concat());
    }
    push(ele) {
        switch (ele.kind) {
            case "field":
                if (!this.left ||
                    this.left.kind != "message" ||
                    this.left.typeName != ele.parent.typeName) {
                    throw this.err("field access");
                }
                this.path.push(ele);
                this.left =
                    ele.fieldKind == "message"
                        ? ele.message
                        : ele.fieldKind == "list" || ele.fieldKind == "map"
                            ? ele
                            : undefined;
                return this;
            case "oneof":
                if (!this.left ||
                    this.left.kind != "message" ||
                    this.left.typeName != ele.parent.typeName) {
                    throw this.err("oneof access");
                }
                this.path.push(ele);
                this.left = undefined;
                return this;
            case "extension":
                if (!this.left ||
                    this.left.kind != "message" ||
                    this.left.typeName != ele.extendee.typeName) {
                    throw this.err("extension access");
                }
                this.path.push(ele);
                this.left = ele.fieldKind == "message" ? ele.message : undefined;
                return this;
            case "list_sub":
                if (!this.left ||
                    this.left.kind != "field" ||
                    this.left.fieldKind != "list") {
                    throw this.err("list access");
                }
                if (ele.index < 0 || !Number.isInteger(ele.index)) {
                    throw this.err("list index");
                }
                this.path.push(ele);
                this.left =
                    this.left.listKind == "message" ? this.left.message : undefined;
                return this;
            case "map_sub":
                if (!this.left ||
                    this.left.kind != "field" ||
                    this.left.fieldKind != "map") {
                    throw this.err("map access");
                }
                if (!checkKeyType(ele.key, this.left.mapKey)) {
                    throw this.err("map key");
                }
                this.path.push(ele);
                this.left =
                    this.left.mapKind == "message" ? this.left.message : undefined;
                return this;
        }
    }
    err(what) {
        return new InvalidPathError(this.schema, "Invalid " + what, this.path);
    }
}
function checkKeyType(key, type) {
    switch (type) {
        case ScalarType.STRING:
            return typeof key == "string";
        case ScalarType.INT32:
        case ScalarType.UINT32:
        case ScalarType.SINT32:
        case ScalarType.SFIXED32:
        case ScalarType.FIXED32:
            return typeof key == "number";
        case ScalarType.UINT64:
        case ScalarType.INT64:
        case ScalarType.FIXED64:
        case ScalarType.SFIXED64:
        case ScalarType.SINT64:
            return typeof key == "bigint";
        case ScalarType.BOOL:
            return typeof key == "boolean";
    }
}
function nextToken(i, path) {
    const re_extension = /^[A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*$/;
    const re_field = /^[A-Za-z_][A-Za-z_0-9]*$/;
    if (path[i] == "[") {
        i++;
        while (path[i] == " ") {
            // skip leading whitespace
            i++;
        }
        if (i >= path.length) {
            return { err: "Premature end", i: path.length - 1 };
        }
        let token;
        if (path[i] == `"`) {
            // string literal
            i++;
            let val = "";
            for (;;) {
                if (path[i] == `"`) {
                    // end of string literal
                    i++;
                    break;
                }
                if (path[i] == "\\") {
                    switch (path[i + 1]) {
                        case `"`:
                        case "\\":
                            val += path[i + 1];
                            break;
                        case "r":
                            val += "\r";
                            break;
                        case "n":
                            val += "\n";
                            break;
                        default:
                            return { err: "Invalid escape sequence", i };
                    }
                    i++;
                }
                else {
                    val += path[i];
                }
                if (i >= path.length) {
                    return { err: "Premature end of string", i: path.length - 1 };
                }
                i++;
            }
            token = { val };
        }
        else if (path[i].match(/\d/)) {
            // integer literal
            const start = i;
            while (i < path.length && /\d/.test(path[i])) {
                i++;
            }
            token = { val: BigInt(path.substring(start, i)) };
        }
        else if (path[i] == "]") {
            return { err: "Premature ]", i };
        }
        else {
            // extension identifier or bool literal
            const start = i;
            while (i < path.length && path[i] != " " && path[i] != "]") {
                i++;
            }
            const name = path.substring(start, i);
            if (name === "true") {
                token = { val: true };
            }
            else if (name === "false") {
                token = { val: false };
            }
            else if (re_extension.test(name)) {
                token = { ext: name };
            }
            else {
                return { err: "Invalid ident", i: start };
            }
        }
        while (path[i] == " ") {
            // skip trailing whitespace
            i++;
        }
        if (path[i] != "]") {
            return { err: "Missing ]", i };
        }
        i++;
        return Object.assign(Object.assign({}, token), { i });
    }
    // field identifier
    if (i > 0) {
        if (path[i] != ".") {
            return { err: `Expected "."`, i };
        }
        i++;
    }
    const start = i;
    while (i < path.length && path[i] != "." && path[i] != "[") {
        i++;
    }
    const field = path.substring(start, i);
    return re_field.test(field)
        ? { field, i }
        : { err: "Invalid ident", i: start };
}
