// Copyright 2021-2024 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 { setEnumType } from "./enum.js";
import { Message } from "../message.js";
import { ScalarType } from "../field.js";
import { scalarEquals } from "./scalars.js";
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-argument,no-case-declarations */
export function makeUtilCommon() {
    return {
        setEnumType,
        initPartial(source, target) {
            if (source === undefined) {
                return;
            }
            const type = target.getType();
            for (const member of type.fields.byMember()) {
                const localName = member.localName, t = target, s = source;
                if (s[localName] === undefined) {
                    continue;
                }
                switch (member.kind) {
                    case "oneof":
                        const sk = s[localName].case;
                        if (sk === undefined) {
                            continue;
                        }
                        const sourceField = member.findField(sk);
                        let val = s[localName].value;
                        if (sourceField &&
                            sourceField.kind == "message" &&
                            !(val instanceof sourceField.T)) {
                            val = new sourceField.T(val);
                        }
                        else if (sourceField &&
                            sourceField.kind === "scalar" &&
                            sourceField.T === ScalarType.BYTES) {
                            val = toU8Arr(val);
                        }
                        t[localName] = { case: sk, value: val };
                        break;
                    case "scalar":
                    case "enum":
                        let copy = s[localName];
                        if (member.T === ScalarType.BYTES) {
                            copy = member.repeated
                                ? copy.map(toU8Arr)
                                : toU8Arr(copy);
                        }
                        t[localName] = copy;
                        break;
                    case "map":
                        switch (member.V.kind) {
                            case "scalar":
                            case "enum":
                                if (member.V.T === ScalarType.BYTES) {
                                    for (const [k, v] of Object.entries(s[localName])) {
                                        t[localName][k] = toU8Arr(v);
                                    }
                                }
                                else {
                                    Object.assign(t[localName], s[localName]);
                                }
                                break;
                            case "message":
                                const messageType = member.V.T;
                                for (const k of Object.keys(s[localName])) {
                                    let val = s[localName][k];
                                    if (!messageType.fieldWrapper) {
                                        // We only take partial input for messages that are not a wrapper type.
                                        // For those messages, we recursively normalize the partial input.
                                        val = new messageType(val);
                                    }
                                    t[localName][k] = val;
                                }
                                break;
                        }
                        break;
                    case "message":
                        const mt = member.T;
                        if (member.repeated) {
                            t[localName] = s[localName].map((val) => val instanceof mt ? val : new mt(val));
                        }
                        else if (s[localName] !== undefined) {
                            const val = s[localName];
                            if (mt.fieldWrapper) {
                                if (
                                // We can't use BytesValue.typeName as that will create a circular import
                                mt.typeName === "google.protobuf.BytesValue") {
                                    t[localName] = toU8Arr(val);
                                }
                                else {
                                    t[localName] = val;
                                }
                            }
                            else {
                                t[localName] = val instanceof mt ? val : new mt(val);
                            }
                        }
                        break;
                }
            }
        },
        equals(type, a, b) {
            if (a === b) {
                return true;
            }
            if (!a || !b) {
                return false;
            }
            return type.fields.byMember().every((m) => {
                const va = a[m.localName];
                const vb = b[m.localName];
                if (m.repeated) {
                    if (va.length !== vb.length) {
                        return false;
                    }
                    // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- repeated fields are never "map"
                    switch (m.kind) {
                        case "message":
                            return va.every((a, i) => m.T.equals(a, vb[i]));
                        case "scalar":
                            return va.every((a, i) => scalarEquals(m.T, a, vb[i]));
                        case "enum":
                            return va.every((a, i) => scalarEquals(ScalarType.INT32, a, vb[i]));
                    }
                    throw new Error(`repeated cannot contain ${m.kind}`);
                }
                switch (m.kind) {
                    case "message":
                        return m.T.equals(va, vb);
                    case "enum":
                        return scalarEquals(ScalarType.INT32, va, vb);
                    case "scalar":
                        return scalarEquals(m.T, va, vb);
                    case "oneof":
                        if (va.case !== vb.case) {
                            return false;
                        }
                        const s = m.findField(va.case);
                        if (s === undefined) {
                            return true;
                        }
                        // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- oneof fields are never "map"
                        switch (s.kind) {
                            case "message":
                                return s.T.equals(va.value, vb.value);
                            case "enum":
                                return scalarEquals(ScalarType.INT32, va.value, vb.value);
                            case "scalar":
                                return scalarEquals(s.T, va.value, vb.value);
                        }
                        throw new Error(`oneof cannot contain ${s.kind}`);
                    case "map":
                        const keys = Object.keys(va).concat(Object.keys(vb));
                        switch (m.V.kind) {
                            case "message":
                                const messageType = m.V.T;
                                return keys.every((k) => messageType.equals(va[k], vb[k]));
                            case "enum":
                                return keys.every((k) => scalarEquals(ScalarType.INT32, va[k], vb[k]));
                            case "scalar":
                                const scalarType = m.V.T;
                                return keys.every((k) => scalarEquals(scalarType, va[k], vb[k]));
                        }
                        break;
                }
            });
        },
        clone(message) {
            const type = message.getType(), target = new type(), any = target;
            for (const member of type.fields.byMember()) {
                const source = message[member.localName];
                let copy;
                if (member.repeated) {
                    copy = source.map(cloneSingularField);
                }
                else if (member.kind == "map") {
                    copy = any[member.localName];
                    for (const [key, v] of Object.entries(source)) {
                        copy[key] = cloneSingularField(v);
                    }
                }
                else if (member.kind == "oneof") {
                    const f = member.findField(source.case);
                    copy = f
                        ? { case: source.case, value: cloneSingularField(source.value) }
                        : { case: undefined };
                }
                else {
                    copy = cloneSingularField(source);
                }
                any[member.localName] = copy;
            }
            return target;
        },
    };
}
// clone a single field value - i.e. the element type of repeated fields, the value type of maps
function cloneSingularField(value) {
    if (value === undefined) {
        return value;
    }
    if (value instanceof Message) {
        return value.clone();
    }
    if (value instanceof Uint8Array) {
        const c = new Uint8Array(value.byteLength);
        c.set(value);
        return c;
    }
    return value;
}
// converts any ArrayLike<number> to Uint8Array if necessary.
function toU8Arr(input) {
    return input instanceof Uint8Array ? input : new Uint8Array(input);
}
