
import * as protobuf from './protobuf.js';

const mediapipe = {};

mediapipe.ModelFactory = class {

    async match(context) {
        const tags = await context.tags('pbtxt');
        if (tags.has('node') && ['input_stream', 'output_stream', 'input_side_packet', 'output_side_packet'].some((key) => tags.has(key) || tags.has(`node.${key}`))) {
            return context.set('mediapipe.pbtxt');
        }
        return null;
    }

    async open(context) {
        // mediapipe.proto = await context.require('./mediapipe-proto');
        mediapipe.proto = {};
        let config = null;
        try {
            const reader = await context.read('protobuf.text');
            // const config = mediapipe.proto.mediapipe.CalculatorGraphConfig.decodeText(reader);
            config = new mediapipe.Object(reader);
        } catch (error) {
            const message = error && error.message ? error.message : error.toString();
            throw new mediapipe.Error(`File text format is not mediapipe.CalculatorGraphConfig (${message.replace(/\.$/, '')}).`);
        }
        return new mediapipe.Model(config);
    }
};

mediapipe.Model = class {

    constructor(config) {
        this.format = 'MediaPipe';
        this.modules = [new mediapipe.Graph(config)];
    }
};

mediapipe.Graph = class {

    constructor(config) {
        config = config || {};
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        const types = new Map();
        const type = (list) => {
            if (!Array.isArray(list)) {
                list = list ? [list] : [];
            }
            return list.map((item) => {
                const parts = item.split(':');
                const name = parts.pop();
                const type = parts.join(':');
                if (!types.has(name)) {
                    const value = new Set();
                    if (type) {
                        value.add(type);
                    }
                    types.set(name, value);
                } else if (type && !types.get(name).has(type)) {
                    types.get(name).add(type);
                }
                return name;
            });
        };
        config.input_stream = type(config.input_stream);
        config.output_stream = type(config.output_stream);
        config.input_side_packet = type(config.input_side_packet);
        config.output_side_packet = type(config.output_side_packet);
        if (!Array.isArray(config.node)) {
            config.node = config.node ? [config.node] : [];
        }
        for (const node of config.node) {
            node.input_stream = type(node.input_stream);
            node.output_stream = type(node.output_stream);
            node.input_side_packet = type(node.input_side_packet);
            node.output_side_packet = type(node.output_side_packet);
        }
        const values = new Map();
        for (const [name, value] of types) {
            const type = Array.from(value).join(',');
            values.set(name, new mediapipe.Value(name, type || null));
        }
        const value = (name) => {
            return values.get(name);
        };
        for (const name of config.input_stream) {
            const argument = new mediapipe.Argument(name, [value(name)]);
            this.inputs.push(argument);
        }
        for (const name of config.output_stream) {
            const argument = new mediapipe.Argument(name, [value(name)]);
            this.outputs.push(argument);
        }
        for (const name of config.input_side_packet) {
            const argument = new mediapipe.Argument(name, [value(name, type)]);
            this.inputs.push(argument);
        }
        for (const output of config.output_side_packet) {
            const parts = output.split(':');
            const type = (parts.length > 1) ? parts.shift() : '';
            const name = parts.shift();
            const argument = new mediapipe.Argument(name, [value(name, type)]);
            this.outputs.push(argument);
        }
        for (const node of config.node) {
            this.nodes.push(new mediapipe.Node(node, value));
        }
    }
};

mediapipe.Node = class {

    constructor(node, value) {
        const type = node.calculator || '?';
        this.name = '';
        this.type = { name: type.replace(/Calculator$/, '') };
        this.inputs = [];
        this.outputs = [];
        this.attributes = [];
        if (node.input_stream) {
            const values = node.input_stream.map((name) => value(name));
            const argument = new mediapipe.Argument('input_stream', values);
            this.inputs.push(argument);
        }
        if (node.output_stream) {
            const values = node.output_stream.map((name) => value(name));
            this.outputs.push(new mediapipe.Argument('output_stream', values));
        }
        if (node.input_side_packet) {
            const values = node.input_side_packet.map((name) => value(name));
            this.inputs.push(new mediapipe.Argument('output_stream', values));
        }
        if (node.output_side_packet) {
            const values = node.output_side_packet.map((name) => value(name));
            this.outputs.push(new mediapipe.Argument('output_side_packet', values));
        }
        const options = new Map();
        if (node.options) {
            for (const key of Object.keys(node.options)) {
                options.set(key, node.options[key]);
            }
        }
        let node_options = node.node_options;
        if (!Array.isArray(node_options)) {
            node_options = node_options ? [node_options] : [];
        }
        if (mediapipe.proto.google && node_options.every((options) => options instanceof mediapipe.proto.google.protobuf.Any)) {
            for (const entry of node_options) {
                const value = new RegExp(/^\{(.*)\}\s*$/, 's').exec(entry.value);
                const buffer = new TextEncoder('utf-8').encode(value[1]);
                const reader = protobuf.TextReader.open(buffer);
                if (entry.type_url.startsWith('type.googleapis.com/mediapipe.')) {
                    const type = entry.type_url.split('.').pop();
                    if (mediapipe.proto && mediapipe.proto.mediapipe && mediapipe.proto.mediapipe[type]) {
                        const message = mediapipe.proto.mediapipe[type].decodeText(reader);
                        for (const key of Object.keys(message)) {
                            options.set(key, message[key]);
                        }
                        continue;
                    }
                }
                const message = new mediapipe.Object(reader);
                for (const [name, value] of Object.entries(message)) {
                    options.set(name, value);
                }
            }
        } else {
            for (const option of node_options) {
                for (const [name, value] of Object.entries(option)) {
                    if (name !== '__type__') {
                        options.set(name, value);
                    }
                }
            }
        }
        for (const [name, value] of options) {
            const attribute = new mediapipe.Argument(name, value);
            this.attributes.push(attribute);
        }
    }
};

mediapipe.Argument = class {

    constructor(name, value) {
        this.name = name;
        this.value = value;
    }
};

mediapipe.Value = class {

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

mediapipe.Object = class {

    constructor(reader, block) {
        if (!block) {
            reader.start();
        }
        const type = reader.token();
        if (type.startsWith('[') && type.endsWith(']')) {
            this.__type__ = type.substring(1, type.length - 1);
            reader.next();
            reader.match(':');
            reader.start();
        }
        const arrayTags = new Set();
        while (!reader.end()) {
            const tag = reader.tag();
            const next = reader.token();
            let obj = null;
            if (next === '{') {
                reader.start();
                obj = new mediapipe.Object(reader, true);
                if (obj.__type__) {
                    while (!reader.end()) {
                        if (!Array.isArray(obj)) {
                            obj = [obj];
                        }
                        const token = reader.token();
                        if (token.startsWith('[') && token.endsWith(']')) {
                            obj.push(new mediapipe.Object(reader, true));
                            continue;
                        }
                        break;
                    }
                }
            } else if (next.startsWith('"') && next.endsWith('"')) {
                obj = next.substring(1, next.length - 1);
                reader.next();
            } else if (next === 'true' || next === 'false') {
                obj = next;
                reader.next();
            } else if (reader.first()) {
                obj = [];
                while (!reader.last()) {
                    const data = reader.token();
                    reader.next();
                    if (!isNaN(data)) {
                        obj.push(parseFloat(data));
                    }
                }
            } else if (isNaN(next)) {
                obj = next;
                reader.next();
            } else {
                obj = parseFloat(next);
                reader.next();
            }
            if (this[tag] && (!Array.isArray(this[tag]) || arrayTags.has(tag))) {
                this[tag] = [this[tag]];
                arrayTags.delete(tag);
            }
            if (this[tag]) {
                this[tag].push(obj);
            } else {
                if (Array.isArray(obj)) {
                    arrayTags.add(tag);
                }
                this[tag] = obj;
            }
            reader.match(',');
        }
    }
};

mediapipe.Error = class extends Error {

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

export const ModelFactory = mediapipe.ModelFactory;
