
import * as base from './base.js';
import * as flatbuffers from './flatbuffers.js';
import * as protobuf from './protobuf.js';
import * as python from './python.js';

const paddle = {};

paddle.ModelFactory = class {

    async match(context) {
        const identifier = context.identifier;
        const extension = identifier.lastIndexOf('.') > 0 ? identifier.split('.').pop().toLowerCase() : '';
        if (identifier === '__model__' || extension === '__model__' || extension === 'paddle' || extension === 'pdmodel') {
            const tags = await context.tags('pb');
            if (tags.get(1) === 2) {
                return context.set('paddle.pb');
            }
        }
        if (extension === 'pbtxt' || extension === 'txt') {
            const tags = await context.tags('pbtxt');
            if (tags.has('blocks')) {
                return context.set('paddle.pbtxt');
            }
        }
        const stream = context.stream;
        if (stream && stream.length > 16 && stream.peek(16).every((value) => value === 0x00)) {
            return context.set('paddle.params');
        }
        const pickle = await paddle.Pickle.open(context);
        if (pickle) {
            return context.set(pickle.name, pickle);
        }
        const entries = await paddle.Entries.open(context);
        if (entries) {
            return context.set(entries.name, entries);
        }
        const naive = await paddle.NaiveBuffer.open(context);
        if (naive) {
            return context.set(naive.name, naive);
        }
        const obj = await context.peek('json');
        if (obj && obj.base_code && obj.program) {
            return context.set('paddle.ir', obj);
        }
        return null;
    }

    filter(context, match) {
        if (context.type === 'paddle.pb' && (match.type === 'paddle.params' || match.type === 'paddle.pickle')) {
            return false;
        }
        if (context.type === 'paddle.naive.model' && match.type === 'paddle.naive.param') {
            return false;
        }
        return true;
    }

    async open(context) {
        const metadata = await context.metadata('paddle-metadata.json');
        switch (context.type) {
            case 'paddle.naive':
            case 'paddle.naive.model':
            case 'paddle.naive.param': {
                paddle.schema = await context.require('./paddle-schema');
                paddle.schema = paddle.schema.paddle.lite.fbs.proto;
                const target = context.value;
                target.read();
                return new paddle.Model(metadata, target.format, target.model, target.weights);
            }
            case 'paddle.ir': {
                const ir = new paddle.IR(context.value);
                const format = `PaddlePaddle IR v${ir.version}`;
                return new paddle.Model(metadata, format, ir.desc, ir.tensors);
            }
            default: {
                paddle.proto = await context.require('./paddle-proto');
                paddle.proto = paddle.proto.paddle.framework.proto;
                const identifier = context.identifier;
                const parts = identifier.split('.');
                const extension = parts.pop().toLowerCase();
                const base = parts.join('.');
                const openProgram = async (context, type) => {
                    const program = {};
                    switch (type) {
                        case 'paddle.pbtxt': {
                            try {
                                const reader = await context.read('protobuf.text');
                                reader.enum = function(type) {
                                    const token = this.token();
                                    this.next();
                                    this.semicolon();
                                    if (type[token] !== undefined) {
                                        return type[token];
                                    }
                                    if (token === 'LOD_TENSOR') {
                                        return type.DENSE_TENSOR;
                                    }
                                    throw new paddle.Error(`Unknown enum value '${token}' ${this.location()}`);
                                };
                                reader.field = function(tag, message) {
                                    if (message instanceof paddle.proto.VarType && tag === 'lod_tensor') {
                                        message.dense_tensor = paddle.proto.VarType.DenseTensorDesc.decodeText(reader);
                                    } else if (message instanceof paddle.proto.VarType.DenseTensorDesc && tag === 'lod_level') {
                                        message.legacy_lod_level = reader.int32();
                                    } else {
                                        throw new Error(`Unknown field '${tag}' ${this.location()}`);
                                    }
                                };
                                program.desc = paddle.proto.ProgramDesc.decodeText(reader);
                            } catch (error) {
                                const message = error && error.message ? error.message : error.toString();
                                throw new paddle.Error(`File text format is not paddle.ProgramDesc (${message.replace(/\.$/, '')}).`);
                            }
                            break;
                        }
                        case 'paddle.pb': {
                            try {
                                const reader = await context.read('protobuf.binary');
                                program.desc = paddle.proto.ProgramDesc.decode(reader);
                            } catch (error) {
                                const message = error && error.message ? error.message : error.toString();
                                throw new paddle.Error(`File format is not paddle.ProgramDesc (${message.replace(/\.$/, '')}).`);
                            }
                            break;
                        }
                        default: {
                            throw new paddle.Error(`Unsupported Paddle format '${type}'.`);
                        }
                    }
                    const formatVersion = (version) => {
                        if (version && version.version !== undefined) {
                            const number = version.version.toNumber();
                            if (number > 0) {
                                const list = [Math.floor(number / 1000000) % 1000, Math.floor(number / 1000) % 1000, number % 1000];
                                if (list.slice(-1).pop() === 0) {
                                    list.pop();
                                    if (list.slice(-1).pop() === 0) {
                                        list.pop();
                                    }
                                }
                                return ` v${list.map((item) => item.toString()).join('.')}`;
                            }
                        }
                        return '';
                    };
                    program.format = `PaddlePaddle${formatVersion(program.desc.version)}`;
                    const variables = new Set();
                    for (const block of program.desc.blocks) {
                        const blockVars = new Set();
                        for (const variable of block.vars) {
                            if (variable.persistable && variable.type &&
                                variable.type.type !== paddle.DataType.FETCH_LIST &&
                                variable.type.type !== paddle.DataType.FEED_MINIBATCH) {
                                blockVars.add(variable.name);
                            }
                        }
                        for (const op of block.ops) {
                            for (const input of op.inputs) {
                                for (const argument of input.arguments) {
                                    if (blockVars.has(argument)) {
                                        variables.add(argument);
                                    }
                                }
                            }
                        }
                    }
                    program.vars = Array.from(variables).sort();
                    return program;
                };
                const loadParams = (stream) => {
                    const params = [];
                    while (stream.position < stream.length) {
                        const tensor = paddle.Utility.openTensorDesc(stream);
                        params.push(tensor);
                    }
                    return params;
                };
                const mapParams = (params, program) => {
                    const weights = new Map();
                    const vars = program.vars.slice();
                    for (const param of params) {
                        weights.set(vars.shift(), param);
                    }
                    return weights;
                };
                switch (context.type) {
                    case 'paddle.pickle': {
                        const target = context.value;
                        return new paddle.Model(metadata, target.format, null, target.weights);
                    }
                    case 'paddle.entries': {
                        const target = context.value;
                        target.read();
                        return new paddle.Model(metadata, target.format, null, target.weights);
                    }
                    case 'paddle.params': {
                        const file = identifier === 'params' ? 'model' : `${base}.pdmodel`;
                        const params = loadParams(context.stream);
                        try {
                            const content = await context.fetch(file);
                            const program = await openProgram(content, 'paddle.pb');
                            const weights = mapParams(params, program);
                            return new paddle.Model(metadata, program.format, program.desc, weights);
                        } catch {
                            const weights = new Map(params.map((param, index) => [index.toString(), param]));
                            return new paddle.Model(metadata, 'PaddlePaddle Inference Weights', null, weights);
                        }
                    }
                    case 'paddle.pb':
                    case 'paddle.pbtxt': {
                        const loadEntries = async (context, program) => {
                            const promises = program.vars.map((name) => context.fetch(name).then((context) => context.stream).catch(() => null));
                            const streams = await Promise.all(promises);
                            const params = streams.map((stream) => stream ? paddle.Utility.openTensorDesc(stream) : null);
                            const weights = mapParams(params, program);
                            return new paddle.Model(metadata, program.format, program.desc, weights);
                        };
                        const openNumPyArrayPickle = (stream) => {
                            const execution = new python.Execution();
                            const pickle = execution.__import__('pickle');
                            const unpickler = new pickle.Unpickler(stream);
                            const obj = unpickler.load();
                            const container = new paddle.Pickle(obj);
                            return container.weights || new Map();
                        };
                        const program = await openProgram(context, context.type);
                        if (extension === 'pdmodel') {
                            try {
                                const name = `${base}.pdiparams`;
                                const content = await context.fetch(name);
                                const params = loadParams(content.stream);
                                const weights = mapParams(params, program);
                                return new paddle.Model(metadata, program.format, program.desc, weights);
                            } catch {
                                try {
                                    const name = `${base}.pdparams`;
                                    const content = await context.fetch(name);
                                    const weights = openNumPyArrayPickle(content.stream);
                                    try {
                                        const name = `${base}.pdopt`;
                                        const content = await context.fetch(name);
                                        for (const [name, value] of openNumPyArrayPickle(content.stream)) {
                                            if (!weights.has(name)) {
                                                weights.set(name, value);
                                            }
                                        }
                                        return new paddle.Model(metadata, program.format, program.desc, weights);
                                    } catch {
                                        return new paddle.Model(metadata, program.format, program.desc, weights);
                                    }
                                } catch {
                                    try {
                                        const name = `${base}.pdopt`;
                                        const content = await context.fetch(name);
                                        const weights = openNumPyArrayPickle(content.stream);
                                        return new paddle.Model(metadata, program.format, program.desc, weights);
                                    } catch {
                                        return loadEntries(context, program);
                                    }
                                }
                            }
                        }
                        if (identifier === 'model') {
                            try {
                                const content = await context.fetch('params');
                                const params = loadParams(content.stream);
                                const weights = mapParams(params, program);
                                return new paddle.Model(metadata, program.format, program.desc, weights);
                            } catch {
                                return loadEntries(context, program);
                            }
                        }
                        return loadEntries(context, program);
                    }
                    default: {
                        throw new paddle.Error(`Unsupported PaddlePaddle format '${context.type}'.`);
                    }
                }
            }
        }
    }
};

paddle.Model = class {

    constructor(metadata, format, desc, tensors) {
        desc = desc && Array.isArray(desc.blocks) ? desc : { blocks: [null] };
        this.format = format;
        this.modules = desc.blocks.map((block) => new paddle.Graph(metadata, block, tensors));
    }
};

paddle.Graph = class {

    constructor(metadata, block, tensors) {
        this.nodes = [];
        this.inputs = [];
        this.outputs = [];
        if (block) {
            this.name = block.idx.toString();
            const values = new Map();
            if (block.kind === 'block') {
                for (const [name, input] of block.argInputs) {
                    const [parameter, tensorType] = input;
                    const value = new paddle.Value(name, tensorType, null, null);
                    values.set(name, value);
                    this.inputs.push(new paddle.Argument(parameter, [value]));
                }
            }
            for (const variable of block.vars) {
                const type = variable.type && variable.type.type && variable.type.dense_tensor && variable.type.dense_tensor.tensor ? paddle.Utility.createTensorType(variable.type.dense_tensor.tensor.data_type, variable.type.dense_tensor.tensor.dims) : null;
                const tensor = variable.persistable && variable.type && variable.type.type !== paddle.DataType.FETCH_LIST && variable.type.type !== paddle.DataType.FEED_MINIBATCH ? (tensors.get(variable.name) || new paddle.Tensor(type)) : null;
                values.set(variable.name, new paddle.Value(variable.name, type, tensor));
            }
            const scope = {};
            for (let i = 0; i < block.ops.length; i++) {
                for (const input of block.ops[i].inputs) {
                    input.arguments = input.arguments.map((argument) => scope[argument] ? scope[argument] : argument);
                }
                for (const output of block.ops[i].outputs) {
                    output.arguments = output.arguments.map((argument) => {
                        if (scope[argument]) {
                            const next = `${argument}\n${i}`; // custom argument id
                            scope[argument] = next;
                            return next;
                        }
                        scope[argument] = argument;
                        return argument;
                    });
                }
            }
            for (const op of block.ops) {
                for (const input of op.inputs) {
                    for (const name of input.arguments) {
                        if (!values.has(name)) {
                            values.set(name, new paddle.Value(name, null, null));
                        }
                    }
                }
                for (const output of op.outputs) {
                    for (const name of output.arguments) {
                        if (output.values && output.values.has(name)) {
                            values.set(name, output.values.get(name));
                        }
                        if (!values.has(name)) {
                            values.set(name, new paddle.Value(name, null, null));
                        }
                    }
                }
            }
            let lastNode = null;
            let lastOutput = null;
            for (const op of block.ops) {
                if (op.type === 'feed') {
                    let name = '';
                    const attr = op.attrs.find((attr) => attr.name === 'col');
                    if (attr) {
                        if (op.kind === 'op') {
                            name = attr.irValue.toString();
                        } else {
                            name = attr.i.toString();
                        }
                    }
                    const argument = new paddle.Argument(name, op.outputs[0].arguments.map((id) => values.get(id)));
                    this.inputs.push(argument);
                } else if (op.type === 'fetch') {
                    let name = '';
                    const attr = op.attrs.find((attr) => attr.name === 'col');
                    if (attr) {
                        if (op.kind === 'op') {
                            name = attr.irValue.toString();
                        } else {
                            name = attr.i.toString();
                        }
                    }
                    const argument = new paddle.Argument(name, op.inputs[0].arguments.map((id) => values.get(id)));
                    this.outputs.push(argument);
                } else {
                    const node = new paddle.Node(metadata, op, values);
                    if (op.inputs.length === 1 && op.inputs[0].arguments.length === 1 &&
                        op.outputs.length >= 1 && op.outputs[0].arguments.length === 1 &&
                        op.inputs[0].arguments[0].split('\n').shift() === op.outputs[0].arguments[0].split('\n').shift() &&
                        lastNode &&
                        lastOutput === op.inputs[0].arguments[0].split('\n').shift()) {
                        lastNode.chain.push(node);
                    } else {
                        this.nodes.push(node);
                        lastNode = null;
                        lastOutput = null;
                        if (op.outputs.length === 1 && op.outputs[0].arguments.length === 1) {
                            lastNode = node;
                            lastOutput = op.outputs[0].arguments[0].split('\n').shift();
                        }
                    }
                }
            }
        } else {
            const values = new Map();
            const ops = new Map();
            for (const [name, tensor] of tensors) {
                values.set(name, new paddle.Value(name, tensor.type, tensor));
                const separator = name.indexOf('.') === -1 ? '_' : '.';
                const regex = /(.*)_((w_attr|scale|weights|offset|b|w|b_attr)_(moment|beta|velocity|mean_square|mean_grad).*)/;
                let parts = [];
                if (separator === '.') {
                    parts = name.split(separator);
                } else if (regex.test(name)) {
                    parts = regex.exec(name).slice(1, 3);
                } else {
                    parts = ['', name];
                }
                const parameter_name = parts.pop();
                const op_name = parts.join(separator);
                if (!ops.has(op_name)) {
                    ops.set(op_name, { name: op_name, type: 'Weights', inputs: [] });
                }
                const op = ops.get(op_name);
                op.inputs.push({ parameter: parameter_name, arguments: [name] });
            }
            for (const op of Array.from(ops.values())) {
                this.nodes.push(new paddle.Node(metadata, op, values));
            }
        }
    }
};

paddle.Argument = class {

    constructor(name, value, type, visible) {
        this.name = name;
        this.value = value;
        if (type) {
            this.type = type;
        }
        if (visible === false) {
            this.visible = visible;
        }
    }
};

paddle.Value = class {

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

paddle.Node = class {

    constructor(metadata, op, values) {
        const type = op.type;
        this.type = metadata.type(type) || { name: type };
        this.name = op.name || '';
        this.description = op.description || '';
        this.identifier = op.identifier || '';
        this.attributes = [];
        this.inputs = [];
        this.outputs = [];
        this.chain = [];
        if (op.attrs) {
            this.attributes = op.attrs.map((attr) => {
                const name = attr.name;
                const meta = metadata.attribute(this.type.name, name);
                let value = '?';
                let visible = true;
                let type = null;
                switch (attr.type) {
                    case paddle.AttributeType.STRING:
                        type = 'string';
                        value = attr.s;
                        break;
                    case paddle.AttributeType.STRINGS:
                        type = 'string[]';
                        value = Array.from(attr.strings);
                        break;
                    case paddle.AttributeType.BOOLEAN:
                        type = 'boolean';
                        value = attr.b;
                        break;
                    case paddle.AttributeType.BOOLEANS:
                        type = 'boolean[]';
                        value = attr.bools ? Array.from(attr.bools) : attr.bools;
                        break;
                    case paddle.AttributeType.FLOAT:
                        type = 'float32';
                        value = attr.f;
                        break;
                    case paddle.AttributeType.FLOATS:
                        type = 'float32[]';
                        value = attr.floats ? Array.from(attr.floats) : attr.floats;
                        break;
                    case paddle.AttributeType.FLOAT64:
                        type = 'float64';
                        value = attr.float64;
                        break;
                    case paddle.AttributeType.FLOAT64S:
                        type = 'float64[]';
                        value = attr.float64s ? Array.from(attr.float64s) : attr.float64s;
                        break;
                    case paddle.AttributeType.INT:
                        type = 'int32';
                        value = attr.i;
                        break;
                    case paddle.AttributeType.INTS:
                        type = 'int32[]';
                        value = attr.ints ? Array.from(attr.ints) : attr.ints;
                        break;
                    case paddle.AttributeType.LONG:
                        type = 'int64';
                        break;
                    case paddle.AttributeType.LONGS:
                        type = 'int64[]';
                        break;
                    case 1000: // ir
                        type = attr.irType;
                        value = attr.irValue;
                        break;
                    case 1001: // graph
                        type = 'graph';
                        value = new paddle.Graph(metadata, attr.block, attr.vars);
                        break;
                    default:
                        break;
                }
                switch (name) {
                    case 'use_mkldnn':
                    case 'use_cudnn':
                    case 'op_callstack':
                    case 'op_role':
                    case 'op_role_var':
                    case 'op_namescope':
                    case 'is_test':
                        visible = false;
                        break;
                    default:
                        break;
                }
                if (meta) {
                    if (meta.default !== undefined) {
                        const defaultValue = meta.default;
                        if (defaultValue === value) {
                            visible = false;
                        } else if (Array.isArray(value) && Array.isArray(defaultValue) && value.length === defaultValue.length) {
                            if (value.every((item, index) => item === defaultValue[index])) {
                                visible = false;
                            }
                        }
                    }
                }
                return new paddle.Argument(name, value, type, visible);
            });
        }
        if (op.inputs) {
            for (const input of op.inputs) {
                if (input.arguments.length > 0) {
                    this.inputs.push(new paddle.Argument(input.parameter, input.arguments.map((name) => values.get(name))));
                }
            }
        }
        if (op.outputs) {
            for (const output of op.outputs) {
                if (output.arguments.length > 0) {
                    this.outputs.push(new paddle.Argument(output.parameter, output.arguments.map((name) => values.get(name))));
                }
            }
        }
        const updates = [
            [this.inputs, 'X'],
            [this.inputs, 'Input'],
            [this.outputs, 'Y'],
            [this.outputs, 'Out']
        ];
        for (const [list, name] of updates) {
            let item = null;
            for (let i = 0; i < list.length; i++) {
                if (list[i].name === name) {
                    item = list[i];
                    list.splice(i, 1);
                    break;
                }
            }
            if (item) {
                list.splice(0, 0, item);
            }
        }
    }
};

paddle.Tensor = class {

    constructor(type, data, category = '') {
        this.type = type;
        this.values = data;
        this.category = category;
    }
};

paddle.TensorType = class {

    constructor(dataType, shape, layout, denotation) {
        this.dataType = dataType;
        this.shape = shape;
        this.layout = layout;
        this.denotation = denotation;
    }

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

paddle.TensorShape = class {

    constructor(dimensions) {
        dimensions = dimensions.map((dim) => typeof dim === 'bigint' ? dim.toNumber() : dim);
        this.dimensions = dimensions.map((dimension) => {
            return dimension === -1 ? '?' : dimension;
        });
    }

    toString() {
        return (this.dimensions && this.dimensions.length) ? (`[${this.dimensions.join(',')}]`) : '';
    }
};

paddle.Entries = class {

    static async open(context) {
        let entries = await context.peek('zip');
        if (entries instanceof Map === false) {
            entries = await context.peek('tar');
        }
        if (entries instanceof Map) {
            entries = Array.from(entries);
            entries = new Map(entries.filter(([name]) => !name.endsWith('/') && !name.split('/').pop().startsWith('.')).slice());
            if (entries.size > 2 && Array.from(entries).every(([name, value]) => name.split('_').length > 0 && value.peek(16).every((value) => value === 0x00))) {
                return new paddle.Entries(entries);
            }
        }
        return null;
    }

    constructor(data) {
        this.name = 'paddle.entries';
        this.format = 'PaddlePaddle Weights';
        this.data = data;
    }

    read() {
        if (this.data) {
            let rootFolder = null;
            for (const [name] of this.data) {
                if (!name.startsWith('.') || name.startsWith('./')) {
                    const parts = name.split('/');
                    let folder = '';
                    if (parts.length > 2 && parts[0] === '.') {
                        folder = `./${parts[1]}/`;
                    } else if (parts.length > 1) {
                        folder = `${parts[0]}/`;
                    }
                    if (rootFolder !== null && rootFolder !== '' && folder !== rootFolder) {
                        rootFolder = '';
                    } else {
                        rootFolder = folder;
                    }
                }
            }
            this.weights = new Map();
            for (const [name, stream] of this.data) {
                if (name.startsWith(rootFolder)) {
                    const key = name.substring(rootFolder.length);
                    const tensor = paddle.Utility.openTensorDesc(stream);
                    this.weights.set(key, tensor);
                }
            }
            delete this.data;
        }
    }
};

paddle.Pickle = class {

    static async open(context) {
        const obj = await context.peek('pkl');
        const container = new paddle.Pickle(obj);
        if (container.weights !== null) {
            return container;
        }
        return null;
    }

    constructor(obj) {
        this.name = 'paddle.pickle';
        this.format = 'PaddlePaddle Pickle';
        this._weights = null;
        if (obj && !Array.isArray(obj) && (obj instanceof Map || Object(obj) === obj)) {
            const entries = (obj) => {
                if (obj instanceof Map) {
                    return Array.from(obj);
                } else if (Object(obj) === obj) {
                    return Object.entries(obj);
                }
                return [];
            };
            const filter = (obj) => {
                const list = [];
                if (obj && !Array.isArray(obj)) {
                    for (const [name, value] of entries(obj)) {
                        if (name !== 'StructuredToParameterName@@') {
                            const obj = value && Array.isArray(value) && value.length === 2 && value[0] === name ? value[1] : value;
                            if (obj && !Array.isArray(obj) && obj.__class__ && obj.__class__.__module__ === 'numpy' && obj.__class__.__name__ === 'ndarray') {
                                list.push([name, obj]);
                            }
                        }
                    }
                }
                return list;
            };
            const weights = filter(obj);
            if (weights.length > 0) {
                this._weights = weights;
            } else {
                const list = entries(obj);
                if (list.filter(([name]) => name !== 'StructuredToParameterName@@').length === 1) {
                    const weights = filter(list[0][1]);
                    if (weights.length > 0) {
                        this._weights = weights;
                    }
                }
                if (this._weights === null && list.filter(([name]) => name === 'StructuredToParameterName@@').length > 0) {
                    this._weights = [];
                }
            }
        }
    }

    get weights() {
        if (this._weights && Array.isArray(this._weights)) {
            const weights = new Map();
            for (const [name, value] of this._weights) {
                const type = new paddle.TensorType(value.dtype.__name__, new paddle.TensorShape(value.shape));
                const data = value.data;
                const tensor = new paddle.Tensor(type, data, 'NumPy Array');
                weights.set(name, tensor);
            }
            this._weights = weights;
        }
        return this._weights;
    }
};

paddle.NaiveBuffer = class {

    static async open(context) {
        const stream = context.stream;
        if (stream && stream.length > 4) {
            const buffer = stream.peek(4);
            if (buffer[0] > 2 || buffer[1] !== 0x00 || buffer[2] !== 0x76 || buffer[3] !== 0x32) {
                if (context.identifier === '__model__.nb') {
                    return new paddle.NaiveBuffer('paddle.naive.model', stream, -1);
                }
                if (context.identifier === 'param.nb') {
                    return new paddle.NaiveBuffer('paddle.naive.param', stream, -1);
                }
            }
            if (buffer[1] === 0x00 && buffer[0] <= 2) {
                return new paddle.NaiveBuffer('paddle.naive', stream, buffer[0]);
            }
        }
        return null;
    }

    constructor(name, stream, meta_version) {
        this.name = name;
        this.stream = stream;
        this.meta_version = meta_version;
    }

    read() {
        const reader = base.BinaryReader.open(this.stream);
        if (this.meta_version >= 2) {
            reader.skip(2);
        }
        const decoder = new TextDecoder('utf-8');
        const opt_version = reader.read(16);
        const version = decoder.decode(opt_version.slice(0, opt_version.indexOf(0x00)));
        this.format = `Paddle Lite${version && version.match(/^v\d+\.\d+\.\d+$/) ? ` ${version}` : ''}`;
        const topo_size = reader.uint64().toNumber();
        const openProgramDesc = (buffer) => {
            const reader = flatbuffers.BinaryReader.open(buffer);
            return paddle.schema.ProgramDesc.create(reader);
        };
        const openParamDesc = (buffer) => {
            const reader = flatbuffers.BinaryReader.open(buffer);
            return paddle.schema.ParamDesc.create(reader);
        };
        switch (this.meta_version) {
            case -1: {
                throw new paddle.Error('Paddle Lite naive buffer format is deprecated.');
            }
            case 0:
            case 1: {
                throw new paddle.Error(`Paddle Lite meta format '${this.meta_version}' is deprecated.`);
            }
            case 2: {
                const topo_data = new Uint8Array(topo_size);
                topo_data.set(reader.read(topo_size), 0);
                this.model = openProgramDesc(topo_data);
                reader.uint16(); // version
                reader.uint16(); // meta_size
                const header_size = reader.uint16();
                const params_size = reader.uint16();
                reader.uint32(); // max_tensor_size
                reader.skip(header_size - 6);
                this.weights = new Map();
                for (let i = 0; i < params_size; i++) {
                    const total_size = reader.uint32();
                    const offset = reader.uint32();
                    const param_bytes = total_size - offset;
                    const param_data = reader.read(param_bytes);
                    const desc = openParamDesc(param_data);
                    const data = desc.variable.data;
                    const data_type = desc.variable.data_type;
                    const dim = desc.variable.dim;
                    const type = paddle.Utility.createTensorType(data_type, dim);
                    const tensor = new paddle.Tensor(type, data);
                    this.weights.set(desc.name, tensor);
                }
                break;
            }
            default: {
                throw new paddle.Error(`Unsupported Paddle Lite naive buffer meta format '${this.meta_version}'.`);
            }
        }
        delete this.stream;
    }
};

paddle.Utility = class {

    static createTensorType(data_type, shape) {
        if (!paddle.Utility._dataTypes) {
            const length = Math.max.apply(null, Object.entries(paddle.DataType).map(([, value]) => value));
            paddle.Utility._dataTypes = new Array(length);
            const types = new Map([
                ['bool', 'boolean'],
                ['bf16', 'bfloat16'], ['fp16', 'float16'], ['fp32', 'float32'], ['fp64', 'float64'],
                ['fp8_e4m3fn', 'float8e4m3fn'], ['fp8_e5m2', 'float8e5m2']
            ]);
            for (const [name, index] of Object.entries(paddle.DataType)) {
                const key = name.toLowerCase();
                paddle.Utility._dataTypes[index] = types.has(key) ? types.get(key) : key;
            }
        }
        const dataType = data_type < paddle.Utility._dataTypes.length ? paddle.Utility._dataTypes[data_type] : '?';
        return new paddle.TensorType(dataType, new paddle.TensorShape(shape));
    }

    static openTensorDesc(stream) {
        const signature = stream.read(16);
        if (!signature.every((value) => value === 0x00)) {
            throw new paddle.Error('Invalid paddle.TensorDesc signature.');
        }
        const length = base.BinaryReader.open(stream.read(4)).uint32();
        const buffer = stream.read(length);
        const reader = protobuf.BinaryReader.open(buffer);
        const tensorDesc = paddle.proto.VarType.TensorDesc.decode(reader);
        const dims = tensorDesc.dims.map((dim) => dim.toNumber());
        const size = dims.reduce((a, b) => a * b, 1);
        let itemsize = 0;
        switch (tensorDesc.data_type) {
            case paddle.DataType.BOOL: itemsize = 1; break;
            case paddle.DataType.FP16: itemsize = 2; break;
            case paddle.DataType.FP32: itemsize = 4; break;
            case paddle.DataType.FP64: itemsize = 8; break;
            case paddle.DataType.INT8: itemsize = 1; break;
            case paddle.DataType.INT16: itemsize = 2; break;
            case paddle.DataType.INT32: itemsize = 4; break;
            case paddle.DataType.INT64: itemsize = 8; break;
            case paddle.DataType.UINT8: itemsize = 1; break;
            default: throw new paddle.Error(`Invalid inference params data type '${tensorDesc.data_type}'.`);
        }
        const type = paddle.Utility.createTensorType(tensorDesc.data_type, tensorDesc.dims);
        const data = stream.read(itemsize * size);
        return new paddle.Tensor(type, data);
    }
};

paddle.DataType = {
    BOOL: 0,
    INT16: 1,
    INT32: 2,
    INT64: 3,
    FP16: 4,
    FP32: 5,
    FP64: 6,
    DENSE_TENSOR: 7,
    SELECTED_ROWS: 8,
    FEED_MINIBATCH: 9,
    FETCH_LIST: 10,
    STEP_SCOPES: 11,
    LOD_RANK_TABLE: 12,
    DENSE_TENSOR_ARRAY: 13,
    PLACE_LIST: 14,
    READER: 15,
    RAW: 17,
    TUPLE: 18,
    SIZE_T: 19,
    UINT8: 20,
    INT8: 21,
    BF16: 22,
    COMPLEX64: 23,
    COMPLEX128: 24,
    STRING: 25,
    STRINGS: 26,
    FP8_E4M3FN: 32,
    FP8_E5M2: 33,
};

paddle.AttributeType = {
    INT: 0,
    FLOAT: 1,
    STRING: 2,
    INTS: 3,
    FLOATS: 4,
    STRINGS: 5,
    BOOLEAN: 6,
    BOOLEANS: 7,
    BLOCK: 8,
    LONG: 9,
    BLOCKS: 10,
    LONGS: 11,
    FLOAT64S: 12,
    VAR: 13,
    VARS: 14,
    FLOAT64: 15
};

paddle.IR = class {

    constructor(obj) {
        this._names = new Map();
        this._crossRegionInputs = new Map();
        this.base_code = obj.base_code;
        this.version = obj.base_code.version;
        const program = obj.program;
        const regions = [];
        for (const region of program.regions) {
            regions.push(this.region(region));
        }
        const [programRegion] = regions;
        this.desc = programRegion;
        this.tensors = new Map();
    }

    region(value) {
        const obj = {};
        obj.kind = 'region';
        obj.name = value['#'];
        obj.idx = value['#'];
        obj.vars = new Map();
        obj.blocks = [];
        for (const block of value.blocks) {
            obj.blocks.push(this.block(block));
        }
        const [block] = obj.blocks;
        obj.block = block;
        return obj;
    }

    block(value) {
        const obj = {};
        obj.kind = 'block';
        obj.name = value['#'];
        obj.idx = value['#'];
        obj.vars = new Map();
        obj.argInputs = new Map();
        if (value.args) {
            for (const input of value.args) {
                const [, type] = input.TT && input.TT['#'] ? input.TT['#'].split('.') : null;
                if (type === 't_dtensor') {
                    const [parameter, name,] = this.getParaName(input);
                    const tensorType = this.createTensorType(input);
                    obj.argInputs.set(name, [parameter, tensorType]);
                }
            }
        }
        let inputNames = new Set();
        let outputNames = new Set();
        obj.ops = [];
        for (const op of value.ops) {
            const irOp = this.op(op);
            obj.ops.push(irOp);
            inputNames = new Set([...inputNames, ...irOp.inputNames]);
            outputNames = new Set([...outputNames, ...irOp.outputNames]);
        }
        const missInputs = new Set([...inputNames].filter((item) => !outputNames.has(item)));
        if (missInputs) {
            for (const name of missInputs) {
                const output = this.getCrossInput(name);
                if (output) {
                    obj.argInputs.set(name, [output.parameter, output.tensorType]);
                }
            }
        }
        return obj;
    }

    op(op) {
        const obj = {};
        obj.kind = 'op';
        const opInfo = this.getOpInfo(op);
        obj.name = opInfo.fullName;
        obj.type = opInfo.type;
        obj.identifier = opInfo.rawType;
        obj.attrs = [];
        for (const [idx, value] of Object.entries(op.A)) {
            obj.attrs.push(this.attr(idx, value, opInfo));
        }
        if (op.regions !== undefined) {
            for (const region of op.regions) {
                const regionAttr = this.region(region);
                obj.attrs.push(this.attr(null, regionAttr, null));
            }
        }
        const inputNames = new Set();
        const outputNames = new Set();
        const createInput = (input, opInfo) => {
            const [parameterName, inputName] = this.getParaName(input, opInfo.namePrefix);
            return { arguments: [inputName], parameter: parameterName };
        };
        const inputs = [];
        if (op.I) {
            const inputArray = Array.isArray(op.I) ? op.I : [op.I];
            for (const input of inputArray) {
                inputs.push(createInput(input, opInfo));
                const [, name] = this.getParaName(input, opInfo.namePrefix);
                inputNames.add(name);
            }
        }
        const createOutput = (output, opInfo, idx, outputAttr) => {
            const [parameterName, outputName] = this.getParaName(output, opInfo.namePrefix);
            const valuesMap = new Map();
            let tType = null;
            const [, typeType] = output.TT['#'].split('.');
            if (typeType === 't_dtensor') {
                const denotation = this.getOutputAttr(opInfo, idx, outputAttr);
                const tensorType = this.createTensorType(output, denotation);
                valuesMap.set(outputName, new paddle.Value(outputName, tensorType, null));
                tType = tensorType;
            } else {
                valuesMap.set(outputName, new paddle.Value(outputName, null, null, null));
            }
            return {
                arguments: [outputName],
                parameter: parameterName,
                tensorType: tType,
                values: valuesMap
            };
        };
        const outputs = [];
        if (op.O) {
            const outputArray = Array.isArray(op.O) ? op.O : [op.O];
            for (const [idx, output] of Object.entries(outputArray)) {
                const irOutput = createOutput(output, opInfo, idx, op.OA);
                outputs.push(irOutput);
                const [, name, isNegative] = this.getParaName(output, opInfo.namePrefix);
                outputNames.add(name);
                if (!isNegative && !this.hasCrossInput(name)) {
                    this.addCrossInput(name, irOutput);
                }
            }
        }
        if (op.regions) {
            const collectRegions = (irReader, regions) => {
                let inputs = new Map();
                let outputs = new Map();
                for (const region of regions) {
                    for (const block of region.blocks) {
                        for (const op of block.ops) {
                            const opInfo = this.getOpInfo(op);
                            if (op.I) {
                                const opInputs = Array.isArray(op.I) ? op.I : [op.I];
                                for (const input of opInputs) {
                                    const [, name, isNegative] = irReader.getParaName(input, opInfo.namePrefix);
                                    if (!isNegative && !inputs.has(name)) {
                                        inputs.set(name, [input, opInfo]);
                                    }
                                }
                            }
                            if (op.O) {
                                const opOutputs = Array.isArray(op.O) ? op.O : [op.O];
                                for (const [idx, output] of Object.entries(opOutputs)) {
                                    const [, name, isNegative] = irReader.getParaName(output, opInfo.namePrefix);
                                    if (!isNegative && !outputs.has(name)) {
                                        outputs.set(name, [output, opInfo, idx, op.OA]);
                                    }
                                }
                            }
                            if (op.regions) {
                                const [subInputs, subOutputs] = collectRegions(irReader, op.regions);
                                inputs = new Map([...inputs, ...subInputs]);
                                outputs = new Map([...outputs, ...subOutputs]);
                            }
                        }
                    }
                }
                return [inputs, outputs];
            };
            const [subInputs, subOutputs] = collectRegions(this, op.regions);
            for (const [name, inputArgs] of subInputs) {
                if (!inputNames.has(name) && !subOutputs.has(name)) {
                    const [input, opInfo] = inputArgs;
                    inputs.push(createInput(input, opInfo));
                    inputNames.add(name);
                }
            }
            for (const [name, outputArgs] of subOutputs) {
                if (!outputNames.has(name) && !subInputs.has(name)) {
                    const [output, opInfo, idx, oa] = outputArgs;
                    outputs.push(createOutput(output, opInfo, idx, oa));
                    outputNames.add(name);
                }
            }
        }
        obj.inputs = inputs;
        obj.outputs = outputs;
        obj.inputNames = inputNames;
        obj.outputNames = outputNames;
        return obj;
    }

    attr(idx, value, opInfo) {
        const obj = {};
        obj.kind = 'attr';
        if (value.kind === 'region') {
            obj.name = value.name;
            obj.type = 1001; // graph
            obj.block = value.block;
            obj.vars = value.vars;
        } else {
            const [attrName, attrType, attrValue] = this.getAttr(opInfo, idx, value);
            obj.name = attrName;
            obj.type = 1000; // ir
            obj.irType = attrType;
            obj.irValue = attrValue;
        }
        return obj;
    }

    getParaName(tensor, namePrefix) {
        let idx = '';
        if ('%' in tensor) {
            idx = tensor['%'];
        } else if ('#' in tensor) {
            idx = tensor['#'];
        }
        if (tensor.TT && !this._names.has(idx)) {
            const prefix = namePrefix || idx;
            this._names.set(idx, `${prefix}`);
        }
        return [`${idx}`, this._names.has(idx) ? this._names.get(idx) : `${idx}`, Number.isInteger(idx) ? idx < 0 : false];
    }

    hasCrossInput(name) {
        return this._crossRegionInputs.has(name);
    }

    getCrossInput(name) {
        return this._crossRegionInputs.has(name) ? this._crossRegionInputs.get(name) : null;
    }

    addCrossInput(name, input) {
        this._crossRegionInputs.set(name, input);
    }

    getOpInfo(op) {
        const obj = {};
        obj.rawType = op['#'];
        obj._type = op['#'];
        obj._name = op['#'];
        switch (op['#']) {
            case 'p': {
                obj.kind = 'p';
                [obj._name] = op.A.slice(3);
                obj._type = this.getCompressOp(obj._type);
                op.OA = [...op.OA, ...op.A];
                obj.type = obj._type;
                obj.name = obj._type;
                obj.fullName = obj._type;
                obj.namePrefix = obj._name;
                break;
            }
            case '1.data': {
                obj.kind = 'data';
                [obj._opKey, obj._opType] = obj._name.split('.');
                let prefix = '';
                for (const attr of op.A) {
                    if (attr.N === 'name') {
                        prefix = attr.AT.D;
                        break;
                    }
                }
                obj._attr = op.A;
                obj.type = obj._opType;
                obj.name = obj._opType;
                obj.fullName = `${this.getCompressOp(obj._opKey)}.${obj._opType}`;
                obj.namePrefix = prefix;
                break;
            }
            default: {
                obj.kind = '';
                [obj._opKey, obj._opType] = obj._name.split('.');
                obj.type = obj._opType;
                obj.name = obj._opType;
                obj.fullName = `${this.getCompressOp(obj._opKey)}.${obj._opType}`;
                obj.namePrefix = null;
                break;
            }
        }
        return obj;
    }

    createTensorType(data, denotation) {
        const [type, dims, layout, ,] = data.TT.D;
        const [, dataType] = type['#'].split('.');
        const dtype = this.getType(dataType);
        const shape = new paddle.TensorShape(dims);
        return new paddle.TensorType(dtype, shape, layout, denotation);
    }

    getType(type) {
        type = type.includes('_') ? type.split('_')[1] : type;
        switch (type) {
            case 'bool': return 'boolean';
            case 'bf16': return 'bfloat16';
            case 'fp16': return 'float16';
            case 'fp32': return 'float32';
            case 'fp64': return 'float64';
            case 'fp8_e4m3fn': return 'float8e4m3fn';
            case 'fp8_e5m2': return 'float8e5m2';
            case 'f8e4m3fn': return 'float8e4m3fn';
            case 'f8e5m2': return 'float8e5m2';
            case 'f16': return 'float16';
            case 'f32': return 'float32';
            case 'f64': return 'float64';
            case 'i8': return 'int8';
            case 'ui8': return 'uint8';
            case 'i16': return 'int16';
            case 'i32': return 'int32';
            case 'i64': return 'int64';
            case 'c64': return 'complex<float32>';
            case 'c128': return 'complex<float64>';
            case 'str': return 'string';
            default: return type;
        }
    }

    getCompressOp(opType) {
        switch (opType) {
            case '0': return 'builtin';
            case '1': return 'pd_op';
            case '2': return 'cf';
            case '3': return 'custom_op';
            case '4': return 'pd_dist';
            case 'p': return 'parameter';
            default: return opType;
        }
    }

    getAttrDenotation(name, value) {
        if (value) {
            if (typeof value === 'boolean') {
                return `${name}`;
            }
            if (name !== 'name' && name !== 'dtype') {
                return `${name}:${value}`;
            }
        }
        return '';
    }

    getAttr(opInfo, idx, value) {
        if (opInfo.kind === 'p') {
            let attrName = '';
            let attrType = '';
            let attrValue = '';
            switch (idx) {
                case '0':
                    attrName = 'is_distributed';
                    attrType = this.getType('a_bool');
                    break;
                case '1':
                    attrName = 'is_parameter';
                    attrType = this.getType('a_bool');
                    break;
                case '2':
                    attrName = 'need_clip';
                    attrType = this.getType('a_bool');
                    break;
                case '3':
                    attrName = 'name';
                    attrType = this.getType('a_str');
                    break;
                default:
                    break;
            }
            attrValue = attrType === this.getType('a_bool') ? value === 1 : value;
            return [attrName, attrType, attrValue];
        }
        const attrName = value.N;
        let attrType = this.getType(value.AT['#'].split('.')[1]);
        let attrValue = value.AT.D;
        if (attrType === this.getType('a_array') && attrValue.length > 0) {
            const subType = this.getType(attrValue[0]['#'].split('.')[1]);
            attrType = `${subType}[]`;
            const valueData = [];
            for (const attr of attrValue) {
                valueData.push(attr.D);
            }
            attrValue = valueData;
        }
        if (attrName === 'place') {
            const [place, val,] = attrValue;
            let device = place;
            switch (device) {
                case 0: device = 'UNDEFINED'; break;
                case 1: device = 'CPU'; break;
                case 2: device = 'GPU'; break;
                case 3: device = 'GPUPINNED'; break;
                case 4: device = 'XPU'; break;
                case 7: device = 'IPU'; break;
                case 9: device = 'CUSTOM'; break;
                default: break;
            }
            attrValue = `${device}:${val}`;
        }
        if (attrName === 'shape') {
            attrValue = new paddle.TensorShape(attrValue);
        }
        return [attrName, attrType, attrValue];
    }

    getOutputAttr(opInfo, idx, outputAttr) {
        switch (opInfo.kind) {
            case 'p': {
                const denotation = [];
                if (outputAttr[0] === 1) {
                    denotation.push('persistable');
                }
                if (outputAttr[1] === 1) {
                    denotation.push('stop_gradient');
                }
                if (outputAttr[2] === 1) {
                    denotation.push('trainable');
                }
                if (outputAttr[3] === 1) {
                    denotation.push('is_distributed');
                }
                if (outputAttr[4] === 1) {
                    denotation.push('is_parameter');
                }
                if (outputAttr[5] === 1) {
                    denotation.push('need_clip');
                }
                return denotation.join(';');
            }
            case 'data': {
                const denotation = [];
                for (const attr of outputAttr) {
                    const attrName = attr.N;
                    const attrValue = attr.AT.D[idx].D;
                    const attrDenotation = this.getAttrDenotation(attrName, attrValue);
                    if (attrDenotation) {
                        denotation.push(attrDenotation);
                    }
                }
                for (const value of opInfo._attr) {
                    const [attrName, , attrValue] = this.getAttr(opInfo, null, value);
                    const attrDenotation = this.getAttrDenotation(attrName, attrValue);
                    if (attrDenotation) {
                        denotation.push(attrDenotation);
                    }
                }
                return denotation.join(';');
            }
            default: {
                const denotation = [];
                for (const attr of outputAttr) {
                    const attrName = attr.N;
                    const attrValue = attr.AT.D[idx].D;
                    const attrDenotation = this.getAttrDenotation(attrName, attrValue);
                    if (attrDenotation) {
                        denotation.push(attrDenotation);
                    }
                }
                return denotation.join(';');
            }
        }
    }
};

paddle.Error = class extends Error {

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

export const ModelFactory = paddle.ModelFactory;