/* eslint-disable  @typescript-eslint/no-explicit-any */
import { Text } from "./text.js";
import { getWriteableCounter } from "./counter.js";
import { STATE, TRACE, IS_PROXY, OBJECT_ID, CLEAR_CACHE, COUNTER, INT, UINT, F64, RAW_STRING, TEXT, } from "./constants.js";
import { RawString } from "./raw_string.js";
function parseListIndex(key) {
    if (typeof key === "string" && /^[0-9]+$/.test(key))
        key = parseInt(key, 10);
    if (typeof key !== "number") {
        return key;
    }
    if (key < 0 || isNaN(key) || key === Infinity || key === -Infinity) {
        throw new RangeError("A list index must be positive, but you passed " + key);
    }
    return key;
}
function valueAt(target, prop) {
    const { context, objectId, path, textV2 } = target;
    const value = context.getWithType(objectId, prop);
    if (value === null) {
        return;
    }
    const datatype = value[0];
    const val = value[1];
    switch (datatype) {
        case undefined:
            return;
        case "map":
            return mapProxy(context, val, textV2, [...path, prop]);
        case "list":
            return listProxy(context, val, textV2, [...path, prop]);
        case "text":
            if (textV2) {
                return context.text(val);
            }
            else {
                return textProxy(context, val, [
                    ...path,
                    prop,
                ]);
            }
        case "str":
            if (textV2) {
                return new RawString(val);
            }
            else {
                return val;
            }
        case "uint":
            return val;
        case "int":
            return val;
        case "f64":
            return val;
        case "boolean":
            return val;
        case "null":
            return null;
        case "bytes":
            return val;
        case "timestamp":
            return val;
        case "counter": {
            const counter = getWriteableCounter(val, context, path, objectId, prop);
            return counter;
        }
        default:
            throw RangeError(`datatype ${datatype} unimplemented`);
    }
}
function import_value(value, textV2, path, context) {
    const type = typeof value;
    switch (type) {
        case "object":
            if (value == null) {
                return [null, "null"];
            }
            else if (value[UINT]) {
                return [value.value, "uint"];
            }
            else if (value[INT]) {
                return [value.value, "int"];
            }
            else if (value[F64]) {
                return [value.value, "f64"];
            }
            else if (value[COUNTER]) {
                return [value.value, "counter"];
            }
            else if (value instanceof Date) {
                return [value.getTime(), "timestamp"];
            }
            else if (isRawString(value)) {
                return [value.toString(), "str"];
            }
            else if (isText(value)) {
                return [value, "text"];
            }
            else if (value instanceof Uint8Array) {
                return [value, "bytes"];
            }
            else if (value instanceof Array) {
                return [value, "list"];
            }
            else if (Object.prototype.toString.call(value) === "[object Object]") {
                return [value, "map"];
            }
            else if (isSameDocument(value, context)) {
                throw new RangeError("Cannot create a reference to an existing document object");
            }
            else {
                throw new RangeError(`Cannot assign unknown object: ${value}`);
            }
        case "boolean":
            return [value, "boolean"];
        case "number":
            if (Number.isInteger(value)) {
                return [value, "int"];
            }
            else {
                return [value, "f64"];
            }
        case "string":
            if (textV2) {
                return [value, "text"];
            }
            else {
                return [value, "str"];
            }
        case "undefined":
            throw new RangeError([
                `Cannot assign undefined value at ${printPath(path)}, `,
                "because `undefined` is not a valid JSON data type. ",
                "You might consider setting the property's value to `null`, ",
                "or using `delete` to remove it altogether.",
            ].join(""));
        default:
            throw new RangeError([
                `Cannot assign ${type} value at ${printPath(path)}. `,
                `All JSON primitive datatypes (object, array, string, number, boolean, null) `,
                `are supported in an Automerge document; ${type} values are not. `,
            ].join(""));
    }
}
// When we assign a value to a property in a proxy we recursively walk through
// the value we are assigning and copy it into the document. This is generally
// desirable behaviour. However, a very common bug is to accidentally assign a
// value which is already in the document to another key within the same
// document, this often leads to surprising behaviour where users expected to
// _move_ the object, but it is instead copied. To avoid this we check if the
// value is from the same document and if it is we throw an error, this means
// we require an explicit Object.assign call to copy the object, thus avoiding
// the footgun
function isSameDocument(val, context) {
    var _b, _c;
    // Date is technically an object, but immutable, so allowing people to assign
    // a date from one place in the document to another place in the document is
    // not likely to be a bug
    if (val instanceof Date) {
        return false;
    }
    // this depends on __wbg_ptr being the wasm pointer
    // a new version of wasm-bindgen will break this
    // but the tests should expose the break
    if (val && ((_c = (_b = val[STATE]) === null || _b === void 0 ? void 0 : _b.handle) === null || _c === void 0 ? void 0 : _c.__wbg_ptr) === context.__wbg_ptr) {
        return true;
    }
    return false;
}
const MapHandler = {
    get(target, key) {
        const { context, objectId, cache } = target;
        if (key === Symbol.toStringTag) {
            return target[Symbol.toStringTag];
        }
        if (key === OBJECT_ID)
            return objectId;
        if (key === IS_PROXY)
            return true;
        if (key === TRACE)
            return target.trace;
        if (key === STATE)
            return { handle: context, textV2: target.textV2 };
        if (!cache[key]) {
            cache[key] = valueAt(target, key);
        }
        return cache[key];
    },
    set(target, key, val) {
        const { context, objectId, path, textV2 } = target;
        target.cache = {}; // reset cache on set
        if (isSameDocument(val, context)) {
            throw new RangeError("Cannot create a reference to an existing document object");
        }
        if (key === TRACE) {
            target.trace = val;
            return true;
        }
        if (key === CLEAR_CACHE) {
            return true;
        }
        const [value, datatype] = import_value(val, textV2, [...path, key], context);
        switch (datatype) {
            case "list": {
                const list = context.putObject(objectId, key, []);
                const proxyList = listProxy(context, list, textV2, [...path, key]);
                for (let i = 0; i < value.length; i++) {
                    proxyList[i] = value[i];
                }
                break;
            }
            case "text": {
                if (textV2) {
                    assertString(value);
                    context.putObject(objectId, key, value);
                }
                else {
                    assertText(value);
                    const text = context.putObject(objectId, key, "");
                    const proxyText = textProxy(context, text, [...path, key]);
                    proxyText.splice(0, 0, ...value);
                }
                break;
            }
            case "map": {
                const map = context.putObject(objectId, key, {});
                const proxyMap = mapProxy(context, map, textV2, [...path, key]);
                for (const key in value) {
                    proxyMap[key] = value[key];
                }
                break;
            }
            default:
                context.put(objectId, key, value, datatype);
        }
        return true;
    },
    deleteProperty(target, key) {
        const { context, objectId } = target;
        target.cache = {}; // reset cache on delete
        context.delete(objectId, key);
        return true;
    },
    has(target, key) {
        const value = this.get(target, key);
        return value !== undefined;
    },
    getOwnPropertyDescriptor(target, key) {
        // const { context, objectId } = target
        const value = this.get(target, key);
        if (typeof value !== "undefined") {
            return {
                configurable: true,
                enumerable: true,
                value,
            };
        }
    },
    ownKeys(target) {
        const { context, objectId } = target;
        // FIXME - this is a tmp workaround until fix the dupe key bug in keys()
        const keys = context.keys(objectId);
        return [...new Set(keys)];
    },
};
const ListHandler = {
    get(target, index) {
        const { context, objectId } = target;
        index = parseListIndex(index);
        if (index === Symbol.hasInstance) {
            return (instance) => {
                return Array.isArray(instance);
            };
        }
        if (index === Symbol.toStringTag) {
            return target[Symbol.toStringTag];
        }
        if (index === OBJECT_ID)
            return objectId;
        if (index === IS_PROXY)
            return true;
        if (index === TRACE)
            return target.trace;
        if (index === STATE)
            return { handle: context };
        if (index === "length")
            return context.length(objectId);
        if (typeof index === "number") {
            return valueAt(target, index);
        }
        else {
            return listMethods(target)[index];
        }
    },
    set(target, index, val) {
        const { context, objectId, path, textV2 } = target;
        index = parseListIndex(index);
        if (isSameDocument(val, context)) {
            throw new RangeError("Cannot create a reference to an existing document object");
        }
        if (index === CLEAR_CACHE) {
            return true;
        }
        if (index === TRACE) {
            target.trace = val;
            return true;
        }
        if (typeof index == "string") {
            throw new RangeError("list index must be a number");
        }
        const [value, datatype] = import_value(val, textV2, [...path, index], context);
        switch (datatype) {
            case "list": {
                let list;
                if (index >= context.length(objectId)) {
                    list = context.insertObject(objectId, index, []);
                }
                else {
                    list = context.putObject(objectId, index, []);
                }
                const proxyList = listProxy(context, list, textV2, [...path, index]);
                proxyList.splice(0, 0, ...value);
                break;
            }
            case "text": {
                if (textV2) {
                    assertString(value);
                    if (index >= context.length(objectId)) {
                        context.insertObject(objectId, index, value);
                    }
                    else {
                        context.putObject(objectId, index, value);
                    }
                }
                else {
                    let text;
                    assertText(value);
                    if (index >= context.length(objectId)) {
                        text = context.insertObject(objectId, index, "");
                    }
                    else {
                        text = context.putObject(objectId, index, "");
                    }
                    const proxyText = textProxy(context, text, [...path, index]);
                    proxyText.splice(0, 0, ...value);
                }
                break;
            }
            case "map": {
                let map;
                if (index >= context.length(objectId)) {
                    map = context.insertObject(objectId, index, {});
                }
                else {
                    map = context.putObject(objectId, index, {});
                }
                const proxyMap = mapProxy(context, map, textV2, [...path, index]);
                for (const key in value) {
                    proxyMap[key] = value[key];
                }
                break;
            }
            default:
                if (index >= context.length(objectId)) {
                    context.insert(objectId, index, value, datatype);
                }
                else {
                    context.put(objectId, index, value, datatype);
                }
        }
        return true;
    },
    deleteProperty(target, index) {
        const { context, objectId } = target;
        index = parseListIndex(index);
        const elem = context.get(objectId, index);
        if (elem != null && elem[0] == "counter") {
            throw new TypeError("Unsupported operation: deleting a counter from a list");
        }
        context.delete(objectId, index);
        return true;
    },
    has(target, index) {
        const { context, objectId } = target;
        index = parseListIndex(index);
        if (typeof index === "number") {
            return index < context.length(objectId);
        }
        return index === "length";
    },
    getOwnPropertyDescriptor(target, index) {
        const { context, objectId } = target;
        if (index === "length")
            return { writable: true, value: context.length(objectId) };
        if (index === OBJECT_ID)
            return { configurable: false, enumerable: false, value: objectId };
        index = parseListIndex(index);
        const value = valueAt(target, index);
        return { configurable: true, enumerable: true, value };
    },
    getPrototypeOf(target) {
        return Object.getPrototypeOf(target);
    },
    ownKeys( /*target*/) {
        const keys = [];
        // uncommenting this causes assert.deepEqual() to fail when comparing to a pojo array
        // but not uncommenting it causes for (i in list) {} to not enumerate values properly
        //const {context, objectId } = target
        //for (let i = 0; i < target.context.length(objectId); i++) { keys.push(i.toString()) }
        keys.push("length");
        return keys;
    },
};
const TextHandler = Object.assign({}, ListHandler, {
    get(target, index) {
        const { context, objectId } = target;
        index = parseListIndex(index);
        if (index === Symbol.hasInstance) {
            return (instance) => {
                return Array.isArray(instance);
            };
        }
        if (index === Symbol.toStringTag) {
            return target[Symbol.toStringTag];
        }
        if (index === OBJECT_ID)
            return objectId;
        if (index === IS_PROXY)
            return true;
        if (index === TRACE)
            return target.trace;
        if (index === STATE)
            return { handle: context };
        if (index === "length")
            return context.length(objectId);
        if (typeof index === "number") {
            return valueAt(target, index);
        }
        else {
            return textMethods(target)[index] || listMethods(target)[index];
        }
    },
    getPrototypeOf( /*target*/) {
        return Object.getPrototypeOf(new Text());
    },
});
export function mapProxy(context, objectId, textV2, path) {
    const target = {
        context,
        objectId,
        path: path || [],
        cache: {},
        textV2,
    };
    const proxied = {};
    Object.assign(proxied, target);
    const result = new Proxy(proxied, MapHandler);
    // conversion through unknown is necessary because the types are so different
    return result;
}
export function listProxy(context, objectId, textV2, path) {
    const target = {
        context,
        objectId,
        path: path || [],
        cache: {},
        textV2,
    };
    const proxied = [];
    Object.assign(proxied, target);
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return new Proxy(proxied, ListHandler);
}
export function textProxy(context, objectId, path) {
    const target = {
        context,
        objectId,
        path: path || [],
        cache: {},
        textV2: false,
    };
    const proxied = {};
    Object.assign(proxied, target);
    return new Proxy(proxied, TextHandler);
}
export function rootProxy(context, textV2) {
    /* eslint-disable-next-line */
    return mapProxy(context, "_root", textV2, []);
}
function listMethods(target) {
    const { context, objectId, path, textV2 } = target;
    const methods = {
        at(index) {
            return valueAt(target, index);
        },
        deleteAt(index, numDelete) {
            if (typeof numDelete === "number") {
                context.splice(objectId, index, numDelete);
            }
            else {
                context.delete(objectId, index);
            }
            return this;
        },
        fill(val, start, end) {
            const [value, datatype] = import_value(val, textV2, [...path, start], context);
            const length = context.length(objectId);
            start = parseListIndex(start || 0);
            end = parseListIndex(end || length);
            for (let i = start; i < Math.min(end, length); i++) {
                if (datatype === "list" || datatype === "map") {
                    context.putObject(objectId, i, value);
                }
                else if (datatype === "text") {
                    if (textV2) {
                        assertString(value);
                        context.putObject(objectId, i, value);
                    }
                    else {
                        assertText(value);
                        const text = context.putObject(objectId, i, "");
                        const proxyText = textProxy(context, text, [...path, i]);
                        for (let i = 0; i < value.length; i++) {
                            proxyText[i] = value.get(i);
                        }
                    }
                }
                else {
                    context.put(objectId, i, value, datatype);
                }
            }
            return this;
        },
        indexOf(o, start = 0) {
            const length = context.length(objectId);
            for (let i = start; i < length; i++) {
                const value = context.getWithType(objectId, i);
                if (value && (value[1] === o[OBJECT_ID] || value[1] === o)) {
                    return i;
                }
            }
            return -1;
        },
        insertAt(index, ...values) {
            this.splice(index, 0, ...values);
            return this;
        },
        pop() {
            const length = context.length(objectId);
            if (length == 0) {
                return undefined;
            }
            const last = valueAt(target, length - 1);
            context.delete(objectId, length - 1);
            return last;
        },
        push(...values) {
            const len = context.length(objectId);
            this.splice(len, 0, ...values);
            return context.length(objectId);
        },
        shift() {
            if (context.length(objectId) == 0)
                return;
            const first = valueAt(target, 0);
            context.delete(objectId, 0);
            return first;
        },
        splice(index, del, ...vals) {
            index = parseListIndex(index);
            // if del is undefined, delete until the end of the list
            if (typeof del !== "number") {
                del = context.length(objectId) - index;
            }
            del = parseListIndex(del);
            for (const val of vals) {
                if (isSameDocument(val, context)) {
                    throw new RangeError("Cannot create a reference to an existing document object");
                }
            }
            const result = [];
            for (let i = 0; i < del; i++) {
                const value = valueAt(target, index);
                if (value !== undefined) {
                    result.push(value);
                }
                context.delete(objectId, index);
            }
            const values = vals.map((val, index) => {
                try {
                    return import_value(val, textV2, [...path], context);
                }
                catch (e) {
                    if (e instanceof RangeError) {
                        throw new RangeError(`${e.message} (at index ${index} in the input)`);
                    }
                    else {
                        throw e;
                    }
                }
            });
            for (const [value, datatype] of values) {
                switch (datatype) {
                    case "list": {
                        const list = context.insertObject(objectId, index, []);
                        const proxyList = listProxy(context, list, textV2, [...path, index]);
                        proxyList.splice(0, 0, ...value);
                        break;
                    }
                    case "text": {
                        if (textV2) {
                            assertString(value);
                            context.insertObject(objectId, index, value);
                        }
                        else {
                            const text = context.insertObject(objectId, index, "");
                            const proxyText = textProxy(context, text, [...path, index]);
                            proxyText.splice(0, 0, ...value);
                        }
                        break;
                    }
                    case "map": {
                        const map = context.insertObject(objectId, index, {});
                        const proxyMap = mapProxy(context, map, textV2, [...path, index]);
                        for (const key in value) {
                            proxyMap[key] = value[key];
                        }
                        break;
                    }
                    default:
                        context.insert(objectId, index, value, datatype);
                }
                index += 1;
            }
            return result;
        },
        unshift(...values) {
            this.splice(0, 0, ...values);
            return context.length(objectId);
        },
        entries() {
            let i = 0;
            const iterator = {
                next: () => {
                    const value = valueAt(target, i);
                    if (value === undefined) {
                        return { value: undefined, done: true };
                    }
                    else {
                        return { value: [i++, value], done: false };
                    }
                },
                [Symbol.iterator]() {
                    return this;
                },
            };
            return iterator;
        },
        keys() {
            let i = 0;
            const len = context.length(objectId);
            const iterator = {
                next: () => {
                    if (i < len) {
                        return { value: i++, done: false };
                    }
                    return { value: undefined, done: true };
                },
                [Symbol.iterator]() {
                    return this;
                },
            };
            return iterator;
        },
        values() {
            let i = 0;
            const iterator = {
                next: () => {
                    const value = valueAt(target, i++);
                    if (value === undefined) {
                        return { value: undefined, done: true };
                    }
                    else {
                        return { value, done: false };
                    }
                },
                [Symbol.iterator]() {
                    return this;
                },
            };
            return iterator;
        },
        toArray() {
            const list = [];
            let value;
            do {
                value = valueAt(target, list.length);
                if (value !== undefined) {
                    list.push(value);
                }
            } while (value !== undefined);
            return list;
        },
        map(f) {
            return this.toArray().map(f);
        },
        toString() {
            return this.toArray().toString();
        },
        toLocaleString() {
            return this.toArray().toLocaleString();
        },
        forEach(f) {
            return this.toArray().forEach(f);
        },
        // todo: real concat function is different
        concat(other) {
            return this.toArray().concat(other);
        },
        every(f) {
            return this.toArray().every(f);
        },
        filter(f) {
            return this.toArray().filter(f);
        },
        find(f) {
            let index = 0;
            for (const v of this) {
                if (f(v, index)) {
                    return v;
                }
                index += 1;
            }
        },
        findIndex(f) {
            let index = 0;
            for (const v of this) {
                if (f(v, index)) {
                    return index;
                }
                index += 1;
            }
            return -1;
        },
        includes(elem) {
            return this.find(e => e === elem) !== undefined;
        },
        join(sep) {
            return this.toArray().join(sep);
        },
        reduce(f, initialValue) {
            return this.toArray().reduce(f, initialValue);
        },
        reduceRight(f, initialValue) {
            return this.toArray().reduceRight(f, initialValue);
        },
        lastIndexOf(search, fromIndex = +Infinity) {
            // this can be faster
            return this.toArray().lastIndexOf(search, fromIndex);
        },
        slice(index, num) {
            return this.toArray().slice(index, num);
        },
        some(f) {
            let index = 0;
            for (const v of this) {
                if (f(v, index)) {
                    return true;
                }
                index += 1;
            }
            return false;
        },
        [Symbol.iterator]: function* () {
            let i = 0;
            let value = valueAt(target, i);
            while (value !== undefined) {
                yield value;
                i += 1;
                value = valueAt(target, i);
            }
        },
    };
    return methods;
}
function textMethods(target) {
    const { context, objectId } = target;
    const methods = {
        set(index, value) {
            return (this[index] = value);
        },
        get(index) {
            return this[index];
        },
        toString() {
            return context.text(objectId).replace(/￼/g, "");
        },
        toSpans() {
            const spans = [];
            let chars = "";
            const length = context.length(objectId);
            for (let i = 0; i < length; i++) {
                const value = this[i];
                if (typeof value === "string") {
                    chars += value;
                }
                else {
                    if (chars.length > 0) {
                        spans.push(chars);
                        chars = "";
                    }
                    spans.push(value);
                }
            }
            if (chars.length > 0) {
                spans.push(chars);
            }
            return spans;
        },
        toJSON() {
            return this.toString();
        },
        indexOf(o, start = 0) {
            const text = context.text(objectId);
            return text.indexOf(o, start);
        },
        insertAt(index, ...values) {
            if (values.every(v => typeof v === "string")) {
                context.splice(objectId, index, 0, values.join(""));
            }
            else {
                listMethods(target).insertAt(index, ...values);
            }
        },
    };
    return methods;
}
function assertText(value) {
    if (!isText(value)) {
        throw new Error("value was not a Text instance");
    }
}
function assertString(value) {
    if (typeof value !== "string") {
        throw new Error("value was not a string");
    }
}
function printPath(path) {
    // print the path as a json pointer
    const jsonPointerComponents = path.map(component => {
        // if its a number just turn it into a string
        if (typeof component === "number") {
            return component.toString();
        }
        else if (typeof component === "string") {
            // otherwise we have to escape `/` and `~` characters
            return component.replace(/~/g, "~0").replace(/\//g, "~1");
        }
    });
    if (path.length === 0) {
        return "";
    }
    else {
        return "/" + jsonPointerComponents.join("/");
    }
}
/*
 * Check if an object is a {@link RawString}
 */
export function isRawString(obj) {
    // We used to determine whether something was a RawString by doing an instanceof check, but
    // this doesn't work if the automerge module is loaded twice somehow. Instead, use the presence
    // of a symbol to determine if something is a RawString
    return (typeof obj === "object" &&
        obj !== null &&
        Object.prototype.hasOwnProperty.call(obj, RAW_STRING));
}
/*
 * Check if an object is a {@link Text}
 */
export function isText(obj) {
    // We used to determine whether something was a Text by doing an instanceof check, but
    // this doesn't work if the automerge module is loaded twice somehow. Instead, use the presence
    // of a symbol to determine if something is a TEXT
    return (typeof obj === "object" &&
        obj !== null &&
        Object.prototype.hasOwnProperty.call(obj, TEXT));
}
