import OP from './OP.js';
import { encode, toHex, toArray } from '../primitives/utils.js';
import BigNumber from '../primitives/BigNumber.js';
/**
 * The Script class represents a script in a Bitcoin SV transaction,
 * encapsulating the functionality to construct, parse, and serialize
 * scripts used in both locking (output) and unlocking (input) scripts.
 *
 * @property {ScriptChunk[]} chunks - An array of script chunks that make up the script.
 */
const BufferCtor = typeof globalThis === 'undefined' ? undefined : globalThis.Buffer;
export default class Script {
    _chunks;
    parsed;
    rawBytesCache;
    hexCache;
    /**
     * @method fromASM
     * Static method to construct a Script instance from an ASM (Assembly) formatted string.
     * @param asm - The script in ASM string format.
     * @returns A new Script instance.
     * @example
     * const script = Script.fromASM("OP_DUP OP_HASH160 abcd... OP_EQUALVERIFY OP_CHECKSIG")
     */
    static fromASM(asm) {
        const chunks = [];
        const tokens = asm.split(' ');
        let i = 0;
        while (i < tokens.length) {
            const { chunk, advance } = Script.parseASMToken(tokens, i);
            chunks.push(chunk);
            i += advance;
        }
        return new Script(chunks);
    }
    static pushdataOpCodeNum(len) {
        if (len >= 0 && len < OP.OP_PUSHDATA1)
            return len;
        if (len < Math.pow(2, 8))
            return OP.OP_PUSHDATA1;
        if (len < Math.pow(2, 16))
            return OP.OP_PUSHDATA2;
        return OP.OP_PUSHDATA4;
    }
    static parseASMToken(tokens, i) {
        const token = tokens[i];
        // Special literal tokens
        if (token === '0')
            return { chunk: { op: 0 }, advance: 1 };
        if (token === '-1')
            return { chunk: { op: OP.OP_1NEGATE }, advance: 1 };
        const isKnownOp = token.startsWith('OP_') && OP[token] !== undefined;
        const opCodeNum = isKnownOp ? OP[token] : 0;
        // Inline PUSHDATA opcodes consume the next two tokens (size, hex data)
        if (opCodeNum === OP.OP_PUSHDATA1 ||
            opCodeNum === OP.OP_PUSHDATA2 ||
            opCodeNum === OP.OP_PUSHDATA4) {
            return { chunk: { data: toArray(tokens[i + 2], 'hex'), op: opCodeNum }, advance: 3 };
        }
        // Unknown opcode token — treat as raw hex push data
        if (!isKnownOp) {
            let hex = token;
            if (hex.length % 2 !== 0)
                hex = '0' + hex;
            const arr = toArray(hex, 'hex');
            if (encode(arr, 'hex') !== hex) {
                throw new Error('invalid hex string in script');
            }
            return { chunk: { data: arr, op: Script.pushdataOpCodeNum(arr.length) }, advance: 1 };
        }
        return { chunk: { op: opCodeNum }, advance: 1 };
    }
    /**
     * @method fromHex
     * Static method to construct a Script instance from a hexadecimal string.
     * @param hex - The script in hexadecimal format.
     * @returns A new Script instance.
     * @example
     * const script = Script.fromHex("76a9...");
     */
    static fromHex(hex) {
        if (hex.length === 0)
            return Script.fromBinary([]);
        if (hex.length % 2 !== 0) {
            throw new Error('There is an uneven number of characters in the string which suggests it is not hex encoded.');
        }
        if (!/^[0-9a-fA-F]+$/.test(hex)) {
            throw new Error('Some elements in this string are not hex encoded.');
        }
        const bin = toArray(hex, 'hex');
        const rawBytes = Uint8Array.from(bin);
        return new Script([], rawBytes, hex.toLowerCase(), false);
    }
    /**
     * @method fromBinary
     * Static method to construct a Script instance from a binary array.
     * @param bin - The script in binary array format.
     * @returns A new Script instance.
     * @example
     * const script = Script.fromBinary([0x76, 0xa9, ...])
     */
    static fromBinary(bin) {
        const rawBytes = Uint8Array.from(bin);
        return new Script([], rawBytes, undefined, false);
    }
    /**
     * @constructor
     * Constructs a new Script object.
     * @param chunks=[] - An array of script chunks to directly initialize the script.
     * @param rawBytesCache - Optional serialized bytes that can be reused instead of reserializing `chunks`.
     * @param hexCache - Optional lowercase hex string that matches the serialized bytes, used to satisfy `toHex` quickly.
     * @param parsed - When false the script defers parsing `rawBytesCache` until `chunks` is accessed; defaults to true.
     */
    constructor(chunks = [], rawBytesCache, hexCache, parsed = true) {
        this._chunks = chunks;
        this.parsed = parsed;
        this.rawBytesCache = rawBytesCache;
        this.hexCache = hexCache;
    }
    get chunks() {
        this.ensureParsed();
        return this._chunks;
    }
    set chunks(value) {
        this._chunks = value;
        this.parsed = true;
        this.invalidateSerializationCaches();
    }
    ensureParsed() {
        if (this.parsed)
            return;
        if (this.rawBytesCache != null) {
            this._chunks = Script.parseChunks(this.rawBytesCache);
        }
        else {
            this._chunks = [];
        }
        this.parsed = true;
    }
    /**
     * @method toASM
     * Serializes the script to an ASM formatted string.
     * @returns The script in ASM string format.
     */
    toASM() {
        let str = '';
        for (const chunk of this.chunks) {
            str += this._chunkToString(chunk);
        }
        return str.slice(1);
    }
    /**
     * @method toHex
     * Serializes the script to a hexadecimal string.
     * @returns The script in hexadecimal format.
     */
    toHex() {
        if (this.hexCache != null) {
            return this.hexCache;
        }
        this.rawBytesCache ??= this.serializeChunksToBytes();
        const hex = BufferCtor == null
            ? encode(Array.from(this.rawBytesCache), 'hex')
            : BufferCtor.from(this.rawBytesCache).toString('hex');
        this.hexCache = hex;
        return hex;
    }
    /**
     * @method toBinary
     * Serializes the script to a binary array.
     * @returns The script in binary array format.
     */
    toBinary() {
        return Array.from(this.toUint8Array());
    }
    toUint8Array() {
        this.rawBytesCache ??= this.serializeChunksToBytes();
        return this.rawBytesCache;
    }
    /**
     * @method writeScript
     * Appends another script to this script.
     * @param script - The script to append.
     * @returns This script instance for chaining.
     */
    writeScript(script) {
        this.invalidateSerializationCaches();
        this.chunks = this.chunks.concat(script.chunks);
        return this;
    }
    /**
     * @method writeOpCode
     * Appends an opcode to the script.
     * @param op - The opcode to append.
     * @returns This script instance for chaining.
     */
    writeOpCode(op) {
        this.invalidateSerializationCaches();
        this.chunks.push({ op });
        return this;
    }
    /**
     * @method setChunkOpCode
     * Sets the opcode of a specific chunk in the script.
     * @param i - The index of the chunk.
     * @param op - The opcode to set.
     * @returns This script instance for chaining.
     */
    setChunkOpCode(i, op) {
        this.invalidateSerializationCaches();
        this.chunks[i] = { op };
        return this;
    }
    /**
     * @method writeBn
     * Appends a BigNumber to the script as an opcode.
     * @param bn - The BigNumber to append.
     * @returns This script instance for chaining.
     */
    writeBn(bn) {
        this.invalidateSerializationCaches();
        if (bn.cmpn(0) === OP.OP_0) {
            this.chunks.push({
                op: OP.OP_0
            });
        }
        else if (bn.cmpn(-1) === 0) {
            this.chunks.push({
                op: OP.OP_1NEGATE
            });
        }
        else if (bn.cmpn(1) >= 0 && bn.cmpn(16) <= 0) {
            // see OP_1 - OP_16
            this.chunks.push({
                op: bn.toNumber() + OP.OP_1 - 1
            });
        }
        else {
            const buf = bn.toSm('little');
            this.writeBin(buf);
        }
        return this;
    }
    /**
     * @method writeBin
     * Appends binary data to the script, determining the appropriate opcode based on length.
     * @param bin - The binary data to append.
     * @returns This script instance for chaining.
     * @throws {Error} Throws an error if the data is too large to be pushed.
     */
    writeBin(bin) {
        this.invalidateSerializationCaches();
        let op;
        const data = bin.length > 0 ? bin : undefined;
        if (bin.length > 0 && bin.length < OP.OP_PUSHDATA1) {
            op = bin.length;
        }
        else if (bin.length === 0) {
            op = OP.OP_0;
        }
        else if (bin.length < Math.pow(2, 8)) {
            op = OP.OP_PUSHDATA1;
        }
        else if (bin.length < Math.pow(2, 16)) {
            op = OP.OP_PUSHDATA2;
        }
        else if (bin.length < Math.pow(2, 32)) {
            op = OP.OP_PUSHDATA4;
        }
        else {
            throw new Error("You can't push that much data");
        }
        this.chunks.push({
            data,
            op
        });
        return this;
    }
    /**
     * @method writeNumber
     * Appends a number to the script.
     * @param num - The number to append.
     * @returns This script instance for chaining.
     */
    writeNumber(num) {
        this.invalidateSerializationCaches();
        this.writeBn(new BigNumber(num));
        return this;
    }
    /**
     * @method removeCodeseparators
     * Removes all OP_CODESEPARATOR opcodes from the script.
     * @returns This script instance for chaining.
     */
    removeCodeseparators() {
        const bytes = this.toUint8Array();
        this.rawBytesCache = Uint8Array.from(Script.removeOpcodeBytes(bytes, OP.OP_CODESEPARATOR));
        this.hexCache = undefined;
        this._chunks = [];
        this.parsed = false;
        return this;
    }
    /**
     * Deletes the given item wherever it appears in the current script.
     *
     * @param script - The script containing the item to delete from the current script.
     *
     * @returns This script instance for chaining.
     */
    findAndDelete(script) {
        this.invalidateSerializationCaches();
        const targetBytes = script.toUint8Array();
        const targetLen = targetBytes.length;
        if (targetLen === 0)
            return this;
        const targetOp = targetBytes[0] ?? 0;
        const matchesChunk = (chunk) => {
            if (chunk.op !== targetOp)
                return false;
            const dataArr = chunk.data ?? [];
            const dataLen = dataArr.length;
            if (dataLen === 0) {
                return targetLen === 1;
            }
            if (chunk.op === OP.OP_RETURN) {
                if (targetLen !== 1 + dataLen)
                    return false;
                for (let j = 0; j < dataLen; j++) {
                    if (targetBytes[1 + j] !== dataArr[j])
                        return false;
                }
                return true;
            }
            if (chunk.op < OP.OP_PUSHDATA1) {
                if (targetLen !== 1 + dataLen)
                    return false;
                for (let j = 0; j < dataLen; j++) {
                    if (targetBytes[1 + j] !== dataArr[j])
                        return false;
                }
                return true;
            }
            if (chunk.op === OP.OP_PUSHDATA1) {
                if (targetLen !== 2 + dataLen)
                    return false;
                if (targetBytes[1] !== (dataLen & 0xff))
                    return false;
                for (let j = 0; j < dataLen; j++) {
                    if (targetBytes[2 + j] !== dataArr[j])
                        return false;
                }
                return true;
            }
            if (chunk.op === OP.OP_PUSHDATA2) {
                if (targetLen !== 3 + dataLen)
                    return false;
                if (targetBytes[1] !== (dataLen & 0xff))
                    return false;
                if (targetBytes[2] !== ((dataLen >> 8) & 0xff))
                    return false;
                for (let j = 0; j < dataLen; j++) {
                    if (targetBytes[3 + j] !== dataArr[j])
                        return false;
                }
                return true;
            }
            if (chunk.op === OP.OP_PUSHDATA4) {
                if (targetLen !== 5 + dataLen)
                    return false;
                const size = dataLen >>> 0;
                if (targetBytes[1] !== (size & 0xff))
                    return false;
                if (targetBytes[2] !== ((size >> 8) & 0xff))
                    return false;
                if (targetBytes[3] !== ((size >> 16) & 0xff))
                    return false;
                if (targetBytes[4] !== ((size >> 24) & 0xff))
                    return false;
                for (let j = 0; j < dataLen; j++) {
                    if (targetBytes[5 + j] !== dataArr[j])
                        return false;
                }
                return true;
            }
            return false;
        };
        for (let i = 0; i < this.chunks.length;) {
            if (matchesChunk(this.chunks[i])) {
                this.chunks.splice(i, 1);
            }
            else {
                i++;
            }
        }
        return this;
    }
    /**
     * @method isPushOnly
     * Checks if the script contains only push data operations.
     * @returns True if the script is push-only, otherwise false.
     */
    isPushOnly() {
        for (const chunk of this.chunks) {
            const opCodeNum = chunk.op;
            if (opCodeNum > OP.OP_16) {
                return false;
            }
        }
        return true;
    }
    /**
     * @method isLockingScript
     * Determines if the script is a locking script.
     * @returns True if the script is a locking script, otherwise false.
     */
    isLockingScript() {
        throw new Error('Not implemented');
    }
    /**
     * @method isUnlockingScript
     * Determines if the script is an unlocking script.
     * @returns True if the script is an unlocking script, otherwise false.
     */
    isUnlockingScript() {
        throw new Error('Not implemented');
    }
    /**
     * @private
     * @method _chunkToString
     * Converts a script chunk to its string representation.
     * @param chunk - The script chunk.
     * @returns The string representation of the chunk.
     */
    static computeSerializedLength(chunks) {
        let total = 0;
        for (const chunk of chunks) {
            total += 1;
            if (chunk.data == null)
                continue;
            const len = chunk.data.length;
            if (chunk.op === OP.OP_RETURN) {
                total += len;
                break;
            }
            if (chunk.op < OP.OP_PUSHDATA1) {
                total += len;
            }
            else if (chunk.op === OP.OP_PUSHDATA1) {
                total += 1 + len;
            }
            else if (chunk.op === OP.OP_PUSHDATA2) {
                total += 2 + len;
            }
            else if (chunk.op === OP.OP_PUSHDATA4) {
                total += 4 + len;
            }
        }
        return total;
    }
    serializeChunksToBytes() {
        const chunks = this.chunks;
        const totalLength = Script.computeSerializedLength(chunks);
        const bytes = new Uint8Array(totalLength);
        let offset = 0;
        for (const chunk of chunks) {
            bytes[offset++] = chunk.op;
            if (chunk.data == null)
                continue;
            if (chunk.op === OP.OP_RETURN) {
                bytes.set(chunk.data, offset);
                break;
            }
            offset = Script.writeChunkData(bytes, offset, chunk.op, chunk.data);
        }
        return bytes;
    }
    invalidateSerializationCaches() {
        this.rawBytesCache = undefined;
        this.hexCache = undefined;
    }
    static writeChunkData(target, offset, op, data) {
        const len = data.length;
        if (op < OP.OP_PUSHDATA1) {
            target.set(data, offset);
            return offset + len;
        }
        else if (op === OP.OP_PUSHDATA1) {
            target[offset++] = len & 0xff;
            target.set(data, offset);
            return offset + len;
        }
        else if (op === OP.OP_PUSHDATA2) {
            target[offset++] = len & 0xff;
            target[offset++] = (len >> 8) & 0xff;
            target.set(data, offset);
            return offset + len;
        }
        else if (op === OP.OP_PUSHDATA4) {
            const size = len >>> 0;
            target[offset++] = size & 0xff;
            target[offset++] = (size >> 8) & 0xff;
            target[offset++] = (size >> 16) & 0xff;
            target[offset++] = (size >> 24) & 0xff;
            target.set(data, offset);
            return offset + len;
        }
        return offset;
    }
    /**
     * Reads pushdata length bytes from `bytes` at `pos` and returns the resulting
     * `{ len, newPos, hasLength }` for a given opcode. Does not read the actual data.
     */
    static readPushdataLength(op, bytes, pos, length) {
        if (op > 0 && op < OP.OP_PUSHDATA1) {
            return { len: op, newPos: pos, hasLength: true };
        }
        if (op === OP.OP_PUSHDATA1) {
            const hasLength = pos < length;
            const len = hasLength ? bytes[pos++] ?? 0 : 0;
            return { len, newPos: pos, hasLength };
        }
        if (op === OP.OP_PUSHDATA2) {
            const hasLength = pos + 1 < length;
            const len = (bytes[pos] ?? 0) | ((bytes[pos + 1] ?? 0) << 8);
            return { len, newPos: Math.min(pos + 2, length), hasLength };
        }
        // OP_PUSHDATA4
        const hasLength = pos + 3 < length;
        const len = ((bytes[pos] ?? 0) |
            ((bytes[pos + 1] ?? 0) << 8) |
            ((bytes[pos + 2] ?? 0) << 16) |
            ((bytes[pos + 3] ?? 0) << 24)) >>> 0;
        return { len, newPos: Math.min(pos + 4, length), hasLength };
    }
    static parseChunks(bytes) {
        const chunks = [];
        const length = bytes.length;
        let pos = 0;
        let inConditionalBlock = 0;
        while (pos < length) {
            const op = bytes[pos++] ?? 0;
            if (op === OP.OP_RETURN && inConditionalBlock === 0) {
                chunks.push({ op, data: Script.copyRange(bytes, pos, length) });
                break;
            }
            if (op === OP.OP_IF || op === OP.OP_NOTIF || op === OP.OP_VERIF || op === OP.OP_VERNOTIF) {
                inConditionalBlock++;
            }
            else if (op === OP.OP_ENDIF) {
                inConditionalBlock--;
            }
            if (op > 0 && op <= OP.OP_PUSHDATA4) {
                const { len, newPos, hasLength } = Script.readPushdataLength(op, bytes, pos, length);
                pos = newPos;
                const end = Math.min(pos + len, length);
                const invalidLength = !hasLength || end - pos !== len;
                chunks.push({ data: Script.copyRange(bytes, pos, end), op, invalidLength });
                pos = end;
            }
            else {
                chunks.push({ op });
            }
        }
        return chunks;
    }
    static removeOpcodeBytes(bytes, opcode) {
        const out = [];
        const length = bytes.length;
        let pos = 0;
        while (pos < length) {
            const start = pos;
            const op = bytes[pos++] ?? 0;
            if (op > 0 && op <= OP.OP_PUSHDATA4) {
                const { len, newPos } = Script.readPushdataLength(op, bytes, pos, length);
                pos = newPos;
                const end = Math.min(pos + len, length);
                if (op !== opcode) {
                    for (let i = start; i < end; i++)
                        out.push(bytes[i] ?? 0);
                }
                pos = end;
            }
            else if (op !== opcode) {
                out.push(op);
            }
        }
        return out;
    }
    static copyRange(bytes, start, end) {
        const size = Math.max(end - start, 0);
        const data = new Array(size);
        for (let i = 0; i < size; i++) {
            data[i] = bytes[start + i] ?? 0;
        }
        return data;
    }
    _chunkToString(chunk) {
        const op = chunk.op;
        let str = '';
        if (chunk.data === undefined) {
            const val = OP[op];
            str = `${str} ${val}`;
        }
        else {
            str = `${str} ${toHex(chunk.data)}`;
        }
        return str;
    }
}
//# sourceMappingURL=Script.js.map