
// Experimental

const executorch = {};
const coreml = {};
const vulkan = {};
const xnnpack = {};
const qnn = {};
const ethosu = {};
const openvino = {};
const rockchip = {};

import * as base from './base.js';
import * as python from './python.js';
import * as pytorch from './pytorch.js';

executorch.ModelFactory = class {

    async match(context) {
        const reader = await executorch.Reader.open(context);
        if (reader) {
            return context.set('executorch', reader);
        }
        return null;
    }

    async open(context) {
        executorch.schema = await context.require('./executorch-schema');
        const target = context.value;
        await target.read();
        return new executorch.Model(target);
    }
};

executorch.Model = class {

    constructor(target) {
        this.format = `ExecuTorch v${target.program.version}`;
        this.modules = [];
        for (const plan of target.program.execution_plan) {
            for (const chain of plan.chains) {
                const graph = new executorch.Graph(target, plan, chain);
                this.modules.push(graph);
            }
        }
    }
};

executorch.Graph = class {

    constructor(target, plan, chain) {
        this.name = plan.name || '';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        const values = new Map();
        values.tensors = (index, items) => {
            const list = [];
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                const type = item ? new executorch.TensorType(item) : null;
                let initializer = null;
                if (item && item.data_buffer_idx > 0) {
                    initializer = new executorch.Tensor(item, target);
                }
                const identifier = items.length > 1 ? `${index}.${i}` : index.toString();
                const value = new executorch.Value(identifier, type, initializer);
                list.push(value);
            }
            return list;
        };
        values.map = (index, output) => {
            if (output && values.has(index) && !Array.isArray(values.get(index).value)) {
                const value = [new executorch.Value(index.toString(), null, null)];
                values.set(index, { type: null, value });
            }
            if (!values.has(index)) {
                const executorch_flatbuffer = executorch.schema.executorch_flatbuffer;
                const val = plan.values[index].val;
                const tensor = val instanceof executorch_flatbuffer.Tensor || val instanceof executorch_flatbuffer.TensorList || val instanceof executorch_flatbuffer.OptionalTensorList;
                if (output && !tensor) {
                    const value = [new executorch.Value(index.toString(), null, null)];
                    values.set(index, { type: null, value });
                } else if (val instanceof executorch_flatbuffer.Null) {
                    values.set(index, { type: 'attribute', value: null });
                } else if (val instanceof executorch_flatbuffer.Int) {
                    values.set(index, { type: 'int64', value: val.int_val });
                } else if (val instanceof executorch_flatbuffer.Bool) {
                    values.set(index, { type: 'int64', value: val.bool_val });
                } else if (val instanceof executorch_flatbuffer.Double) {
                    values.set(index, { type: 'float64', value: val.double_val });
                } else if (val instanceof executorch_flatbuffer.Tensor) {
                    const items = [val];
                    values.set(index, { type: null, value: values.tensors(index, items) });
                } else if (val instanceof executorch_flatbuffer.String) {
                    values.set(index, { type: 'string', value: val.string_val });
                } else if (val instanceof executorch_flatbuffer.IntList) {
                    const list = val.items.map((index) => plan.values[index].val.int_val);
                    values.set(index, { type: 'int64[]', value: list });
                } else if (val instanceof executorch_flatbuffer.DoubleList) {
                    values.set(index, { type: 'float64[]', value: Array.from(val.items) });
                } else if (val instanceof executorch_flatbuffer.BoolList) {
                    throw new executorch.Error('executorch_flatbuffer.BoolList not implemented.');
                } else if (val instanceof executorch_flatbuffer.TensorList) {
                    const items = Array.from(val.items).map((arg) => arg === -1 ? null : plan.values[arg].val);
                    values.set(index, { type: null, value: values.tensors(index, items) });
                } else if (val instanceof executorch_flatbuffer.OptionalTensorList) {
                    const items = Array.from(val.items).map((arg) => arg === -1 ? null : plan.values[arg].val);
                    values.set(index, { type: null, value: values.tensors(index, items) });
                } else {
                    throw new Error(`Value type '${val.constructor.name}' not implemented.`);
                }
            }
            return values.get(index);
        };
        for (let i = 0; i < plan.inputs.length; i++) {
            const input = plan.inputs[i];
            const value = values.map(input);
            const name = plan.inputs.length === 1 ? 'input' : `input.${i}`;
            const argument = new executorch.Argument(name, value.value, value.type);
            this.inputs.push(argument);
        }
        for (let i = 0; i < plan.outputs.length; i++) {
            const output = plan.outputs[i];
            const value = values.map(output);
            const name = plan.outputs.length === 1 ? 'output' : `output.${i}`;
            const argument = new executorch.Argument(name, value.value, value.type);
            this.outputs.push(argument);
        }
        const executorch_flatbuffer = executorch.schema.executorch_flatbuffer;
        for (const instruction of chain.instructions) {
            const instr_args = instruction.instr_args;
            if (instr_args instanceof executorch_flatbuffer.JumpFalseCall ||
                instr_args instanceof executorch_flatbuffer.MoveCall ||
                instr_args instanceof executorch_flatbuffer.FreeCall) {
                continue;
            }
            const node = new executorch.Node(target, plan, chain, instruction, values);
            this.nodes.push(node);
        }
    }
};

executorch.Argument = class {

    constructor(name, value, type = null, visible = true) {
        this.name = name;
        this.value = value;
        this.type = type;
        this.visible = visible;
    }
};

executorch.Value = class Value {

    constructor(name, type, initializer = null) {
        if (typeof name !== 'string') {
            throw new executorch.Error(`Invalid value identifier '${JSON.stringify(name)}'.`);
        }
        this.name = name;
        this.type = initializer && initializer.type ? initializer.type : type || null;
        this.initializer = initializer;
    }
};

executorch.Node = class {

    constructor(target, plan, chain, instruction, values) {
        this.name = '';
        this.inputs = [];
        this.outputs = [];
        this.attributes = [];
        const instr_args = instruction.instr_args;
        const executorch_flatbuffer = executorch.schema.executorch_flatbuffer;
        if (instr_args instanceof executorch_flatbuffer.KernelCall) {
            const op = plan.operators[instr_args.op_index];
            const name = op.name.split('::').pop();
            const identifier = op.overload ? `${op.name}.${op.overload}` : op.name;
            const torch = target.execution.__import__('torch');
            const schemas = torch._C._jit_get_schemas_for_operator(op.name);
            const schema = schemas.find((schema) => schema.name === op.name && schema.overload_name === op.overload);
            if (!schema) {
                throw new executorch.Error(`Operator schema for '${identifier}' not found.`);
            }
            const category = schema && schema.category ? schema.category : '';
            const alias = (arg) => arg && arg.alias_info && arg.alias_info.before_set.length === 1 ? arg.alias_info.before_set[0] : null;
            const outputs = new Set(schema && Array.isArray(schema.returns) ? schema.returns.map((arg) => alias(arg)).filter((alias) => alias !== null) : []);
            const inputs = new Map();
            this.type = { name, identifier, category };
            let i = 0;
            const args = instr_args.args;
            for (; i < schema.arguments.length; i++) {
                const index = args[i];
                const arg = schema && i < schema.arguments.length ? schema.arguments[i] : null;
                const output = arg ? alias(schema.arguments[i]) : null;
                if (output && outputs.has(output)) {
                    inputs.set(output, index);
                    continue;
                }
                const name = arg ? arg.name : i.toString();
                const value = values.map(index);
                const argument = new executorch.Argument(name, value.value, value.type);
                this.inputs.push(argument);
            }
            for (let j = 0; j < schema.returns.length; j++) {
                const ret = schema.returns[j];
                const output = alias(ret);
                let index = args[i++];
                index = output && inputs.has(output) ? inputs.get(output) : index;
                const name = ret.name;
                const value = values.map(index, true);
                const argument = new executorch.Argument(name || '', value.value, value.type);
                this.outputs.push(argument);
            }
        } else if (instr_args instanceof executorch_flatbuffer.DelegateCall) {
            const delegate = plan.delegates[instr_args.delegate_index];
            const args = instr_args.args;
            if (!delegate.backend || !delegate.backend.type) {
                throw new executorch.Error(`ExecuTorch delegate '${delegate.id}' not implemented.`);
            }
            this.type = delegate.backend.type;
            const inputs = args.slice(0, this.type.inputs.length);
            for (let i = 0; i < inputs.length; i++) {
                const input = inputs[i];
                const value = values.map(input);
                const name = inputs.length === 1 ? 'input' : `input.${i}`;
                const argument = new executorch.Argument(name, value.value, value.type);
                this.inputs.push(argument);
            }
            const outputs = args.slice(this.type.inputs.length, this.type.inputs.length + this.type.outputs.length);
            for (let i = 0; i < outputs.length; i++) {
                const output = outputs[i];
                const value = values.map(output);
                const name = outputs.length === 1 ? 'output' : `output.${i}`;
                const argument = new executorch.Argument(name, value.value, value.type);
                this.outputs.push(argument);
            }
            for (const spec of delegate.compile_specs) {
                const value = spec.value instanceof Uint8Array ? new TextDecoder('utf-8').decode(spec.value) : spec.value;
                const attribute = new executorch.Argument(spec.key, value, 'attribute');
                this.attributes.push(attribute);
            }
        } else {
            throw new Error(`Instruction type '${instr_args.constructor.name}' not implemented.`);
        }
    }
};

executorch.TensorType = class {

    constructor(tensor) {
        const ScalarType = executorch.schema.executorch_flatbuffer.ScalarType;
        switch (tensor.scalar_type) {
            case ScalarType.BYTE: this.dataType = 'uint8'; break;
            case ScalarType.CHAR: this.dataType = 'int8'; break;
            case ScalarType.SHORT: this.dataType = 'int16'; break;
            case ScalarType.INT: this.dataType = 'int32'; break;
            case ScalarType.LONG: this.dataType = 'int64'; break;
            case ScalarType.HALF: this.dataType = 'float16'; break;
            case ScalarType.FLOAT: this.dataType = 'float32'; break;
            case ScalarType.DOUBLE: this.dataType = 'float64'; break;
            case ScalarType.BFLOAT16: this.dataType = 'bfloat16'; break;
            case 8: this.dataType = 'complex<float16>'; break;
            case 9: this.dataType = 'complex<float32>'; break;
            case 10: this.dataType = 'complex<float64>'; break;
            case ScalarType.BOOL: this.dataType = 'boolean'; break;
            case ScalarType.QINT8: this.dataType = 'qint8'; break;
            case ScalarType.QUINT8: this.dataType = 'quint8'; break;
            case ScalarType.QINT32: this.dataType = 'qint32'; break;
            case 15: this.dataType = 'bfloat16'; break;
            case ScalarType.QUINT4X2: this.dataType = 'quint4x2'; break;
            case ScalarType.QUINT2X4: this.dataType = 'quint2x4'; break;
            case 18: this.dataType = 'bits1x8'; break;
            case 19: this.dataType = 'bits2x4'; break;
            case 20: this.dataType = 'bits4x2'; break;
            case 21: this.dataType = 'bits8'; break;
            case ScalarType.BITS16: this.dataType = 'bits16'; break;
            case ScalarType.FLOAT8E5M2: this.dataType = 'float8e5m2'; break;
            case ScalarType.FLOAT8E4M3FN: this.dataType = 'float8e4m3fn'; break;
            case ScalarType.FLOAT8E5M2FNUZ: this.dataType = 'float8e5m2fnuz'; break;
            case ScalarType.FLOAT8E4M3FNUZ: this.dataType = 'float8e4m3fnuz'; break;
            case ScalarType.UINT16: this.dataType = 'uint16'; break;
            case ScalarType.UINT32: this.dataType = 'uint32'; break;
            case ScalarType.UINT64: this.dataType = 'uint64'; break;
            default: throw new executorch.Error(`Unknown tensor data type '${tensor.scalar_type}'.`);
        }
        this.shape = new executorch.TensorShape(Array.from(tensor.sizes));
    }

    toString() {
        return this.dataType + this.shape.toString();
    }
};

executorch.TensorShape = class {

    constructor(dimensions = []) {
        this.dimensions = dimensions;
    }

    toString() {
        if (this.dimensions && this.dimensions.length > 0) {
            return `[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]`;
        }
        return '';
    }
};

executorch.Tensor = class {

    constructor(tensor, target) {
        this.type = new executorch.TensorType(tensor);
        const data_buffer_idx = tensor.data_buffer_idx;
        const program = target.program;
        if (tensor.extra_tensor_info) {
            throw new executorch.Error('Extra tensor info not implemented.');
        } else if (Array.isArray(program.constant_buffer) && program.constant_buffer.length > 0) {
            if (data_buffer_idx >= program.constant_buffer.length) {
                throw new executorch.Error(`Constant buffer index out of range.`);
            }
            const buffer = program.constant_buffer[data_buffer_idx];
            this.values = buffer.storage;
            this.encoding = '<';
        } else if (tensor.allocation_info === null) {
            const constant_segment = program.constant_segment;
            const data_segment = program.segments[constant_segment.segment_index];
            const offset = constant_segment.offsets[data_buffer_idx];
            let next = data_segment.size;
            if (data_buffer_idx + 1 < constant_segment.offsets.length) {
                next = constant_segment.offsets[data_buffer_idx + 1];
            }
            const size = next - offset;
            const position = data_segment.offset + offset;
            this.values = target.blob(position.toNumber(), size.toNumber());
            this.encoding = '<';
        } else {
            throw new executorch.Error('Tensor allocation info not implemented.');
        }
    }
};

executorch.Reader = class {

    static async open(context) {
        const reader = await context.peek('flatbuffers.binary');
        if (reader && reader.identifier === 'ET12') {
            return new executorch.Reader(context, reader);
        }
        return null;
    }

    constructor(context, reader) {
        this.context = context;
        this.reader = reader;
    }

    async read() {
        const context = this.context;
        this.metadata = await pytorch.Metadata.open(context);
        this.execution = new python.Execution();
        this.metadata.register(this.execution);
        const executorch_flatbuffer = executorch.schema.executorch_flatbuffer;
        this.program = executorch_flatbuffer.Program.create(this.reader);
        this.named_data = new Map();
        if (this.program.named_data) {
            this.named_data = new Map(this.program.named_data.map((entry) => [entry.key, entry.segment_index]));
        }
        this.reader = await context.read('binary');
        if (this.reader.length >= 32) {
            this.reader.seek(8);
            const magic = String.fromCharCode(...this.reader.read(4));
            if (magic === 'eh00') {
                this.extended_file_header = {
                    length: this.reader.uint32(),
                    program_size: this.reader.uint64(),
                    segment_base_offset: this.reader.uint64(),
                };
            }
            this.reader.seek(0);
        }
        for (const plan of this.program.execution_plan) {
            for (const chain of plan.chains) {
                for (const instruction of chain.instructions) {
                    const instr_args = instruction.instr_args;
                    if (instr_args instanceof executorch_flatbuffer.DelegateCall) {
                        const delegate = plan.delegates[instr_args.delegate_index];
                        if (delegate.backend) {
                            continue;
                        }
                        let data = null;
                        switch (delegate.processed.location) {
                            case executorch_flatbuffer.DataLocation.INLINE: {
                                data = this.program.backend_delegate_data[delegate.processed.index].data;
                                break;
                            }
                            case executorch_flatbuffer.DataLocation.SEGMENT: {
                                const segment = this.program.segments[delegate.processed.index];
                                const offset = segment.offset;
                                const size = segment.size;
                                data = this.blob(offset.toNumber(), size.toNumber());
                                break;
                            }
                            default: {
                                throw new executorch.Error(`Delegate data location '${delegate.processed.location}' not implemented.`);
                            }
                        }
                        switch (delegate.id) {
                            case 'XnnpackBackend':
                                delegate.backend = xnnpack.Reader.open(data, this);
                                break;
                            case 'CoreMLBackend':
                                delegate.backend = coreml.Reader.open(data, this);
                                break;
                            case 'VulkanBackend':
                                delegate.backend = vulkan.Reader.open(data, this);
                                break;
                            case 'QnnBackend':
                                delegate.backend = qnn.Reader.open(data, this);
                                break;
                            case 'EthosUBackend':
                                delegate.backend = ethosu.Reader.open(data, this);
                                break;
                            case 'OpenvinoBackend':
                                delegate.backend = openvino.Reader.open(data, this);
                                break;
                            case 'RockchipBackend':
                                delegate.backend = rockchip.Reader.open(data, this);
                                break;
                            default:
                                throw new executorch.Error(`ExecuTorch delegate '${delegate.id}' not implemented.`);
                        }
                        // eslint-disable-next-line no-await-in-loop
                        await delegate.backend.read();
                    }
                }
            }
        }
    }

    blob(offset, size) {
        if (this.extended_file_header) {
            const segment_base_offset = this.extended_file_header.segment_base_offset;
            this.reader.seek(segment_base_offset.toNumber() + offset);
            const data = this.reader.read(size);
            this.reader.seek(0);
            return data;
        }
        return null;
    }

    segment(key) {
        if (this.named_data.has(key)) {
            const segment_index = this.named_data.get(key);
            if (segment_index >= 0 && segment_index < this.program.segments.length) {
                const segment = this.program.segments[segment_index];
                const offset = segment.offset;
                const size = segment.size;
                return this.blob(offset.toNumber(), size.toNumber());
            }
        }
        return null;
    }
};

executorch.Error = class extends Error {

    constructor(message) {
        super(message);
        this.name = 'Error loading ExecuTorch model.';
    }
};

xnnpack.Reader = class {

    static open(data, target) {
        if (data.length >= 30) {
            const reader = base.BinaryReader.open(data);
            reader.skip(4);
            const magic = String.fromCharCode(...reader.read(4));
            if (magic === 'XH00') {
                return new xnnpack.Reader(reader, target);
            }
        }
        return null;
    }

    constructor(reader, target) {
        this.reader = reader;
        this.target = target;
        reader.skip(2);
        this.flatbuffer = {
            offset: reader.uint32(),
            size: reader.uint32(),
        };
        this.constants = {
            offset: reader.uint32(),
            size: reader.uint32(),
        };
    }

    async read() {
        this.reader.seek(this.flatbuffer.offset);
        const flatbuffers = await import('./flatbuffers.js');
        const data = this.reader.read(this.flatbuffer.size);
        const reader = flatbuffers.BinaryReader.open(data);
        if (!executorch.schema.fb_xnnpack.XNNGraph.identifier(reader)) {
            throw new xnnpack.Error('Invalid XNNPACK data.');
        }
        this.graph = executorch.schema.fb_xnnpack.XNNGraph.create(reader);
        this.reader.seek(0);
        const metadata = new xnnpack.Metadata();
        this.type = new xnnpack.Graph(metadata, this.graph, this);
    }

    constant(idx) {
        const constant_data = this.graph.constant_data[idx];
        const named_key = constant_data.named_key;
        if (named_key) {
            return this.target.segment(named_key);
        }
        const offset = constant_data.offset;
        const size = constant_data.size;
        this.reader.seek(this.constants.offset + offset.toNumber());
        const data = this.reader.read(size.toNumber());
        this.reader.seek(0);
        return data;
    }
};

xnnpack.Graph = class {

    constructor(metadata, graph, reader) {
        this.name = 'XnnpackBackend';
        this.type = 'graph';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        const values = new Map();
        values.map = (id) => {
            if (!values.has(id)) {
                const fb_xnnpack = executorch.schema.fb_xnnpack;
                const name = id.toString();
                const xvalue = graph.xvalues[id].xvalue_union;
                if (xvalue instanceof fb_xnnpack.XNNTensorValue) {
                    const type = new xnnpack.TensorType(xvalue);
                    const initializer = xvalue.constant_buffer_idx === 0 ? null : new xnnpack.Tensor(xvalue, reader);
                    const value = new xnnpack.Value(name, type, initializer);
                    values.set(id, value);
                } else if (xvalue instanceof fb_xnnpack.XNNQuantizedTensorValue) {
                    const value = new xnnpack.Value(name, null, null);
                    values.set(id, value);
                } else {
                    throw new xnnpack.Error(`Value type '${xvalue.constructor.name}' not implemented.`);
                }
            }
            return values.get(id);
        };
        for (let i = 0; i < graph.input_ids.length; i++) {
            const id = graph.input_ids[i];
            const value = values.map(id);
            const name = graph.input_ids.length === 1 ? 'input' : `input.${i}`;
            const argument = new xnnpack.Argument(name, [value]);
            this.inputs.push(argument);
        }
        for (let i = 0; i < graph.output_ids.length; i++) {
            const id = graph.output_ids[i];
            const value = values.map(id);
            const name = graph.output_ids.length === 1 ? 'output' : `output.${i}`;
            const argument = new xnnpack.Argument(name, [value]);
            this.outputs.push(argument);
        }
        for (const xnode of graph.xnodes) {
            const node = new xnnpack.Node(metadata, xnode, values);
            this.nodes.push(node);
        }
    }
};

xnnpack.Node = class {

    constructor(metadata, xnode, values) {
        const node = xnode.xnode_union;
        this.type = metadata.type(node.constructor.name) || { name: node.constructor.name };
        this.name = '';
        this.inputs = [];
        this.outputs = [];
        for (const [name, obj] of Object.entries(node)) {
            let value = ArrayBuffer.isView(obj) ? Array.from(obj) : obj;
            let type = 'attribute';
            if (name.endsWith('_id')) {
                value = obj === -1 || obj === 0xFFFFFFFF ? [] : [values.map(obj)];
                type = null;
            }
            const argument = new xnnpack.Argument(name, value, type);
            if (name === 'output_id') {
                this.outputs.push(argument);
            } else {
                this.inputs.push(argument);
            }
        }
    }
};

xnnpack.Argument = class {

    constructor(name, value, type = null, visible = true) {
        this.name = name;
        this.value = value;
        this.type = type;
        this.visible = visible;
    }
};

xnnpack.Value = class Value {

    constructor(name, type, initializer = null) {
        if (typeof name !== 'string') {
            throw new executorch.Error(`Invalid value identifier '${JSON.stringify(name)}'.`);
        }
        this.name = name;
        this.type = initializer && initializer.type ? initializer.type : type || null;
        this.initializer = initializer;
    }
};

xnnpack.Metadata = class {

    constructor() {
        this._types = new Map();
        this.register('_XNNCat', 'Tensor');
        this.register('_XNNNodeConv', 'Layer');
        this.register('XNNArgMaxPooling2d', 'Pool');
        this.register('XNNAvgPooling2d', 'Pool');
        this.register('XNNCeiling', 'Activation');
        this.register('XNNConcatenate2', 'Tensor');
        this.register('XNNConcatenate3', 'Tensor');
        this.register('XNNConcatenate4', 'Tensor');
        this.register('XNNConcatenate5', 'Tensor');
        this.register('XNNConv2d', 'Layer');
        this.register('XNNConvTranspose2d', 'Layer');
        this.register('XNNDepthwiseConv2d', 'Layer');
        this.register('XNNELU', 'Activation');
        this.register('XNNFullyConnected', 'Layer');
        this.register('XNNGelu', 'Activation');
        this.register('XNNGlobalAvgPooling2d', 'Pool');
        this.register('XNNGlobalAvgPooling2d', 'Pool');
        this.register('XNNHardswish', 'Activation');
        this.register('XNNLeakyReLU', 'Activation');
        this.register('XNNMaxPooling2d', 'Pool');
        this.register('XNNPReLU', 'Activation');
        this.register('XNNSigmoid', 'Activation');
        this.register('XNNSoftmax', 'Activation');
        this.register('XNNTanh', 'Activation');
        this.register('XNNStaticTranspose', 'Transform');
    }

    register(name, category) {
        this._types.set(name, { name, category });
    }

    type(name) {
        return this._types.get(name);
    }
};

xnnpack.TensorType = class {

    constructor(tensor) {
        xnnpack.TensorType._types = executorch.TensorType._types || [
            'invalid', 'float32', 'float16',
            'qint8', 'quint8', 'qint32',
            'qcint8', 'qcint32', 'qcint4',
            'qdint8', 'qbint4', 'qpint8',
            'int32', 'pfp32', 'bfloat16'
        ];
        if (tensor.datatype >= xnnpack.TensorType._types.length) {
            throw new xnnpack.Error(`Unknown tensor data type '${tensor.datatype}'.`);
        }
        this.dataType = xnnpack.TensorType._types[tensor.datatype];
        this.shape = new xnnpack.TensorShape(Array.from(tensor.dims));
    }

    toString() {
        return this.dataType + this.shape.toString();
    }
};

xnnpack.TensorShape = class {

    constructor(dimensions = []) {
        this.dimensions = dimensions;
    }

    toString() {
        if (this.dimensions && this.dimensions.length > 0) {
            return `[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]`;
        }
        return '';
    }
};

xnnpack.Tensor = class {

    constructor(tensor, reader) {
        this.type = new xnnpack.TensorType(tensor);
        this.values = reader.constant(tensor.constant_buffer_idx);
        this.encoding = '<';
    }
};

xnnpack.Error = class extends Error {

    constructor(message) {
        super(message);
        this.name = 'Error loading XNNPACK model.';
    }
};

vulkan.Reader = class {

    static open(data, target) {
        if (data.length >= 30) {
            const reader = base.BinaryReader.open(data);
            reader.skip(4);
            const magic = String.fromCharCode(...reader.read(4));
            if (magic === 'VH00') {
                return new vulkan.Reader(reader, target);
            }
        }
        return null;
    }

    constructor(reader, target) {
        this.reader = reader;
        this.target = target;
        reader.skip(2);
        this.flatbuffer = {
            offset: reader.uint32(),
            size: reader.uint32(),
        };
        this.constants = {
            offset: reader.uint32(),
            size: reader.uint32(),
        };
    }

    async read() {
        this.reader.seek(this.flatbuffer.offset);
        const metadata = new vulkan.Metadata(this.target.execution);
        metadata.register('conv_with_clamp(Tensor input, Tensor weight, Tensor? bias, SymInt[] stride, SymInt[] padding, SymInt[] dilation, bool transposed, SymInt[] output_padding, SymInt groups, Scalar? output_min, Scalar? output_max) -> Tensor)');
        const flatbuffers = await import('./flatbuffers.js');
        const data = this.reader.read(this.flatbuffer.size);
        const reader = flatbuffers.BinaryReader.open(data);
        if (!executorch.schema.vkgraph.VkGraph.identifier(reader)) {
            throw new xnnpack.Error('Invalid Vuklan data.');
        }
        this.graph = executorch.schema.vkgraph.VkGraph.create(reader);
        this.reader.seek(0);
        this.type = new vulkan.Graph(metadata, this.graph, this);
    }

    constant(id) {
        const constant = this.graph.constants[id];
        const offset = constant.offset;
        const length = constant.length;
        this.reader.seek(this.constants.offset + offset.toNumber());
        const data = this.reader.read(length.toNumber());
        this.reader.seek(0);
        return data;
    }
};

vulkan.Graph = class {

    constructor(metadata, graph, reader) {
        this.name = 'VulkanBackend';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        const values = new Map();
        values.map = (id) => {
            if (!values.has(id)) {
                const vkgraph = executorch.schema.vkgraph;
                const arg = graph.values[id].value;
                if (arg instanceof vkgraph.VkTensor) {
                    const type = new vulkan.TensorType(arg);
                    const initializer = arg.constant_id === -1 ? null : new vulkan.Tensor(arg, reader);
                    const value = new vulkan.Value(id.toString(), type, initializer);
                    values.set(id, { type: null, value: [value] });
                } else if (arg instanceof vkgraph.Int) {
                    values.set(id, { type: 'int64', value: arg.int_val });
                } else if (arg instanceof vkgraph.IntList) {
                    values.set(id, { type: 'int64[]', value: Array.from(arg.items) });
                } else if (arg instanceof vkgraph.Double) {
                    values.set(id, { type: 'float64', value: arg.double_val });
                } else if (arg instanceof vkgraph.Bool) {
                    values.set(id, { type: 'boolean', value: arg.bool_val });
                } else if (arg instanceof vkgraph.Null) {
                    values.set(id, { type: 'attribute', value: null });
                } else {
                    throw new Error(`Value type '${arg.constructor.name}' not implemented.`);
                }
            }
            return values.get(id);
        };
        for (let i = 0; i < graph.input_ids.length; i++) {
            const id = graph.input_ids[i];
            const value = values.map(id);
            const name = graph.input_ids.length === 1 ? 'input' : `input.${i}`;
            const argument = new vulkan.Argument(name, value.value, value.type);
            this.inputs.push(argument);
        }
        for (let i = 0; i < graph.output_ids.length; i++) {
            const id = graph.output_ids[i];
            const value = values.map(id);
            const name = graph.output_ids.length === 1 ? 'output' : `output.${i}`;
            const argument = new vulkan.Argument(name, value.value, value.type);
            this.outputs.push(argument);
        }
        for (const op of graph.chain) {
            const node = new vulkan.Node(metadata, op, values);
            this.nodes.push(node);
        }
    }
};

vulkan.Node = class {

    constructor(metadata, op, values) {
        const schema = metadata.type(op.name);
        if (!schema) {
            throw new vulkan.Error(`Operator schema for '${op.name}' not found.`);
        }
        this.type = {
            name: op.name.split(/\.([^.]*)$/)[0],
            identifier: op.name,
            category: schema.category || ''
        };
        this.name = op.node_id.toString();
        this.inputs = [];
        this.outputs = [];
        this.attributes = [];
        for (let i = 0; i < op.args.length; i++) {
            const arg = op.args[i];
            const input = schema && i < schema.arguments.length;
            const def = input ? schema.arguments[i] : schema.returns[i - schema.arguments.length];
            const value = values.map(arg);
            const argument = new vulkan.Argument(def.name || '', value.value, value.type);
            if (input) {
                this.inputs.push(argument);
            } else {
                this.outputs.push(argument);
            }
        }

    }
};

vulkan.Argument = class {

    constructor(name, value, type = null, visible = true) {
        this.name = name;
        this.value = value;
        this.type = type;
        this.visible = visible;
    }
};

vulkan.Value = class Value {

    constructor(name, type, initializer = null) {
        if (typeof name !== 'string') {
            throw new executorch.Error(`Invalid value identifier '${JSON.stringify(name)}'.`);
        }
        this.name = name;
        this.type = initializer && initializer.type ? initializer.type : type || null;
        this.initializer = initializer;
    }
};

vulkan.TensorType = class {

    constructor(tensor) {
        const types = ['bool', 'uint8', 'int8', 'int32', 'float16', 'float32', 'float64', 'int64'];
        if (tensor.datatype >= types.length) {
            throw new vulkan.Error(`Unknown tensor data type '${tensor.datatype}'.`);
        }
        this.dataType = types[tensor.datatype];
        this.shape = new vulkan.TensorShape(Array.from(tensor.dims));
        const vkgraph = executorch.schema.vkgraph;
        if (tensor.memory_layout !== vkgraph.VkMemoryLayout.DEFAULT_LAYOUT) {
            this.denotation = vkgraph.VkMemoryLayout[tensor.memory_layout];
            if (!this.denotation) {
                throw new vulkan.Error(`Unsupported memory layout '${tensor.memory_layout}'.`);
            }
        }
        if (tensor.storage_type !== vkgraph.VkStorageType.DEFAULT_STORAGE) {
            this.layout = vkgraph.VkStorageType[tensor.storage_type];
            if (!this.layout) {
                throw new vulkan.Error(`Unsupported storage type '${tensor.storage_type}'.`);
            }
        }
    }

    toString() {
        return this.dataType + this.shape.toString();
    }
};

vulkan.TensorShape = class {

    constructor(dimensions = []) {
        this.dimensions = dimensions;
    }

    toString() {
        if (this.dimensions && this.dimensions.length > 0) {
            return `[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]`;
        }
        return '';
    }
};

vulkan.Tensor = class {

    constructor(tensor, reader) {
        this.type = new vulkan.TensorType(tensor);
        this.values = reader.constant(tensor.constant_id);
        this.encoding = '<';
    }
};

vulkan.Metadata = class {

    constructor(execution) {
        this.execution = execution;
    }

    register(signature) {
        const torch = this.execution.__import__('torch');
        const registry = torch._C.getRegistry();
        const schema = torch.FunctionSchema.parse(signature);
        const op = new torch._C.Operator(schema);
        registry.registerOperator(op);
    }

    type(identifier) {
        identifier = identifier.split(/\.([^.]*)$/);
        const name = identifier[0].replace('.', '::');
        const overload = identifier[1] === 'default' ? '' : identifier[1];
        const torch = this.execution.__import__('torch');
        const schemas = torch._C._jit_get_schemas_for_operator(name);
        const schema = schemas.find((schema) => schema.name === name && schema.overload_name === overload);
        return schema;
    }
};

vulkan.Error = class extends Error {

    constructor(message) {
        super(message);
        this.name = 'Error loading Vulkan model.';
    }
};

coreml.Reader = class {

    static open(data, target) {
        const reader = base.BinaryReader.open(data);
        return new coreml.Reader(reader, target);
    }

    constructor(reader, target) {
        this.reader = reader;
        this.target = target;
    }

    async factory() {
        const coreml = await import('./coreml.js');
        return new coreml.ModelFactory();
    }

    async read() {
        const entries = this.entries(this.reader);
        const factory = await this.factory();
        const streams = new Map();
        for (const [path, location] of entries) {
            streams.set(path, this.stream(location.offset, location.size));
        }
        const context = this.target.context;
        for (const [key] of streams) {
            const content = context.context(key, streams.get(key), streams);
            // eslint-disable-next-line no-await-in-loop
            const type = await factory.match(content);
            if (type === 'coreml.manifest') {
                // eslint-disable-next-line no-await-in-loop
                const model = await factory.open(content);
                [this.type] = model.modules;
                this.type.name = 'CoreMLBackend';
                return;
            }
        }
    }

    stream(offset, size) {
        this.reader.seek(offset);
        const stream = this.reader.stream(size);
        this.reader.seek(0);
        return stream;
    }

    entries(reader) {
        const files = new Map();
        reader.seek(reader.length - 1);
        const str = [];
        let depth = 0;
        do {
            const c = String.fromCharCode(reader.byte());
            reader.skip(-2);
            if (c === '{') {
                depth++;
            } else if (c === '}') {
                depth--;
            }
            str.push(c);
        } while (depth > 0);
        const metadata = JSON.parse(str.join(''));
        const nodes = metadata.nodes;
        const roots = Array.from(nodes);
        for (const root of roots) {
            if (root !== null) {
                for (const index of Object.values(root.children)) {
                    roots[index] = null;
                }
            }
        }
        const process = (path, node) => {
            path = path ? `${path}/${node.name}` : node.name;
            if (node.kind === 0) {
                files.set(path, node.dataRegion);
            } else if (node.kind === 1) {
                for (const index of Object.values(node.children)) {
                    process(path, nodes[index]);
                }
            } else {
                throw new Error(`Node kind '${node.kind}' not implemented.`);
            }
        };
        for (const root of roots.filter((node) => node !== null)) {
            process('', root);
        }
        return files;
    }
};

qnn.Reader = class {

    static open(data, target) {
        if (data.length >= 20) {
            const reader = base.BinaryReader.open(data);
            const magic = reader.uint32();
            if (magic === 0x5678ABCD) {
                return new qnn.Reader(reader, target);
            }
        }
        return null;
    }

    constructor(reader, target) {
        this.reader = reader;
        this.target = target;
        this.signature = reader.uint64();
        this.size = reader.uint64();
    }

    async read() {
        // https://github.com/pytorch/executorch/blob/main/backends/qualcomm/runtime/backends/QnnCustomProtocol.h
        throw new executorch.Error('Undocumented QNN backend not implemented.');
    }
};

qnn.Graph = class {

    constructor() {
        this.name = 'QnnBackend';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
    }
};

ethosu.Reader = class {

    static open(data /* , target */) {
        if (data.length >= 32) {
            const reader = base.BinaryReader.open(data);
            const magicBuffer = reader.read(16);
            const magic = String.fromCharCode(...magicBuffer).replace(/\0/g, '');
            if (magic === 'vela_bin_stream') {
                return new ethosu.Reader(reader, data.length);
            }
        }
        return null;
    }

    constructor(reader, size) {
        this.reader = reader;
        this.size = size;
    }

    async read() {
        this.reader.seek(0);
        const blocks = new Map();
        while (this.reader.position < this.size) {
            const nameBuffer = this.reader.read(16);
            const name = String.fromCharCode(...nameBuffer).replace(/\0/g, '');
            const size = this.reader.uint32();
            this.reader.skip(12);
            const data = this.reader.read(size);
            blocks.set(name, data);
            const padding = (16 - (size % 16)) % 16;
            this.reader.skip(padding);
            if (name === 'vela_end_stream') {
                break;
            }
        }
        const args = (data) => {
            if (data && data.length >= 4) {
                const reader = base.BinaryReader.open(data);
                const count = reader.int32();
                const arg = [];
                for (let i = 0; i < count; i++) {
                    const shape = [];
                    for (let j = 0; j < 6; j++) {
                        shape.push(reader.int32());
                    }
                    const elem_size = reader.int32();
                    const offset = reader.int32();
                    const region = reader.int32();
                    arg.push({ shape, elem_size, offset, region });
                }
                return arg;
            }
            return [];
        };
        const inputs = args(blocks.get('inputs'));
        const outputs = args(blocks.get('outputs'));
        this.type = new ethosu.Graph(inputs, outputs);
    }
};

ethosu.Graph = class {

    constructor(inputs, outputs) {
        this.name = 'EthosUBackend';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        for (let i = 0; i < inputs.length; i++) {
            const input = inputs[i];
            const type = new ethosu.TensorType(input);
            const value = new ethosu.Value(i.toString(), type, null);
            const name = inputs.length === 1 ? 'input' : `input.${i}`;
            const argument = new ethosu.Argument(name, [value]);
            this.inputs.push(argument);
        }
        for (let i = 0; i < outputs.length; i++) {
            const output = outputs[i];
            const type = new ethosu.TensorType(output);
            const value = new ethosu.Value((inputs.length + i).toString(), type, null);
            const name = outputs.length === 1 ? 'output' : `output.${i}`;
            const argument = new ethosu.Argument(name, [value]);
            this.outputs.push(argument);
        }
    }
};

ethosu.Argument = class {

    constructor(name, value, type = null, visible = true) {
        this.name = name;
        this.value = value;
        this.type = type;
        this.visible = visible;
    }
};

ethosu.Value = class Value {

    constructor(name, type, initializer = null) {
        if (typeof name !== 'string') {
            throw new executorch.Error(`Invalid value identifier '${JSON.stringify(name)}'.`);
        }
        this.name = name;
        this.type = initializer && initializer.type ? initializer.type : type || null;
        this.initializer = initializer;
    }
};

ethosu.TensorType = class {

    constructor(io) {
        switch (io.elem_size) {
            case 1: this.dataType = 'int8'; break;
            case 2: this.dataType = 'int16'; break;
            case 4: this.dataType = 'int32'; break;
            default: this.dataType = `?${io.elem_size}`; break;
        }
        const shape = io.shape.filter((dim, index) => dim !== 1 || index === io.shape.length - 1 || io.shape.slice(index).some((d) => d !== 1));
        this.shape = new ethosu.TensorShape(shape.length > 0 ? shape : [1]);
    }

    toString() {
        return this.dataType + this.shape.toString();
    }
};

ethosu.TensorShape = class {

    constructor(dimensions = []) {
        this.dimensions = dimensions;
    }

    toString() {
        if (this.dimensions && this.dimensions.length > 0) {
            return `[${this.dimensions.map((dimension) => dimension.toString()).join(',')}]`;
        }
        return '';
    }
};

ethosu.Error = class extends Error {

    constructor(message) {
        super(message);
        this.name = 'Error loading Ethos-U model.';
    }
};

openvino.Reader = class {

    static open(data /* , target */) {
        return new openvino.Reader(data);
    }

    constructor(data) {
        this.data = data;
    }

    async read() {
        throw new executorch.Error('OpenVINO backend not implemented.');
    }
};

openvino.Graph = class {

    constructor() {
        this.name = 'OpenvinoBackend';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
    }
};

rockchip.Reader = class {

    static open(data /* , target */) {
        return new rockchip.Reader(data);
    }

    constructor(data) {
        this.data = data;
    }

    async read() {
        throw new executorch.Error('Rockchip backend not implemented.');
    }
};

rockchip.Graph = class {

    constructor() {
        this.name = 'RockchipBackend';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
    }
};

export const ModelFactory = executorch.ModelFactory;