
// Experimental

const barracuda = {};

barracuda.ModelFactory = class {

    async match(context) {
        const stream = context.stream;
        if (stream && stream.length > 12) {
            const buffer = stream.peek(12);
            if (buffer[0] <= 0x20 && buffer.subarray(1, 8).every((value) => value === 0x00)) {
                return context.set('barracuda');
            }
        }
        return null;
    }

    async open(context) {
        const metadata = barracuda.Metadata.open();
        const reader = await context.read('binary');
        const model = new barracuda.NNModel(reader);
        return new barracuda.Model(metadata, model);
    }
};

barracuda.Model = class {

    constructor(metadata, model) {
        const version = model.version.toString();
        this.format = `Barracuda v${version}`;
        this.modules = [new barracuda.Graph(metadata, model)];
    }
};

barracuda.Graph = class {

    constructor(metadata, model) {
        this.name = '';
        this.inputs = [];
        this.outputs = [];
        this.nodes = [];
        const values = new Map();
        values.map = (name, type, tensor) => {
            if (!values.has(name)) {
                type = tensor ? tensor.type : type;
                values.set(name, new barracuda.Value(name, type, tensor));
            } else if (type || tensor) {
                throw new barracuda.Error(`Duplicate value '${name}'.`);
            }
            return values.get(name);
        };
        const layers = [];
        for (const layer of model.layers) {
            if (layer.type !== 255 || layer.inputs.length > 0) {
                layers.push(layer);
            } else {
                for (const tensor of layer.tensors) {
                    values.map(tensor.name, null, new barracuda.Tensor(tensor));
                }
            }
        }
        for (const input of model.inputs) {
            const shape = new barracuda.TensorShape(input.shape);
            const type = new barracuda.TensorType(4, shape);
            const argument = new barracuda.Argument(input.name, [values.map(input.name, type)]);
            this.inputs.push(argument);
        }
        for (const output of model.outputs) {
            const argument = new barracuda.Argument(output, [values.map(output)]);
            this.outputs.push(argument);
        }
        for (const layer of layers) {
            const node = new barracuda.Node(metadata, layer, null, values);
            this.nodes.push(node);
        }
    }
};

barracuda.Argument = class {

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

barracuda.Value = class {

    constructor(name, type = null, initializer = null) {
        this.name = name;
        this.type = type;
        this.initializer = initializer;
    }
};

barracuda.Node = class {

    constructor(metadata, layer, type, values) {
        this.name = layer.name || '';
        this.type = type ? type : metadata.type(layer.type);
        this.inputs = [];
        this.outputs = [];
        this.attributes = [];
        const inputs = Array.prototype.slice.call(this.type.inputs || ['input']);
        if (this.type.inputs && this.type.inputs.length === 1 && this.type.inputs[0].name === 'inputs') {
            const argument = new barracuda.Argument('inputs', layer.inputs.map((input) => values.map(input)));
            this.inputs.push(argument);
        } else if (layer.inputs) {
            for (let i = 0; i < layer.inputs.length; i++) {
                const input = layer.inputs[i];
                const name = inputs.length > 0 && inputs[0] ? inputs.shift().name : i.toString();
                const argument = new barracuda.Argument(name, [values.map(input)]);
                this.inputs.push(argument);
            }
        }
        if (layer.tensors) {
            for (let i = 0; i < layer.tensors.length; i++) {
                const tensor = layer.tensors[i];
                const initializer = new barracuda.Tensor(tensor);
                const name = inputs.length > 0 && inputs[0] ? inputs.shift().name : i.toString();
                const argument = new barracuda.Argument(name, [values.map(tensor.name, initializer.type, initializer)]);
                this.inputs.push(argument);
            }
        }
        if (layer.inputs !== undefined) {
            const argument = new barracuda.Argument('output', [values.map(this.name)]);
            this.outputs.push(argument);
        }
        if (layer.activation !== undefined && (layer.type === 50 || layer.activation !== 0)) {
            const type = barracuda.Activation[layer.activation];
            if (!type) {
                throw new barracuda.Error(`Unsupported activation '${layer.activation}'.`);
            }
            const node = new barracuda.Node(metadata, {}, { name: type, category: 'Activation' }, values);
            this.chain = [node];
        }
        const attributes = [
            ['strides', 'int32[]', []],
            ['pads', 'int32[]', (value) => Array.isArray(value) && (value.every((v) => v === 0) || value.every((v) => v === -1))],
            ['pool_size', 'int32[]', []],
            ['alpha', 'float32', 1],
            ['beta', 'float32', 0],
            ['axis', 'int32', -1]
        ];
        for (const [name, type, defaultValue] of attributes) {
            const value = layer[name];
            if ((value === undefined) ||
                (Array.isArray(defaultValue) && Array.isArray(value) && value.length === defaultValue.length && value.every((v, i) => v === defaultValue[i])) ||
                (typeof defaultValue === 'function' && defaultValue(value)) ||
                (defaultValue === value)) {
                continue;
            }
            const attribute = new barracuda.Argument(name, value, type);
            this.attributes.push(attribute);
        }
    }
};

barracuda.Tensor = class {

    constructor(tensor) {
        this.type = new barracuda.TensorType(tensor.itemsize, new barracuda.TensorShape(tensor.shape));
        this.values = tensor.data;
    }
};

barracuda.TensorType = class {

    constructor(itemsize, shape) {
        switch (itemsize) {
            case 4: this.dataType = 'float32'; break;
            default: throw new barracuda.Error(`Unsupported data type size '${itemsize}'.`);
        }
        this.shape = shape;
    }

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

barracuda.TensorShape = class {

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

    toString() {
        return this.dimensions ? (`[${this.dimensions.map((dimension) => dimension ? dimension.toString() : '?').join(',')}]`) : '';
    }
};

barracuda.NNModel = class {

    constructor(reader) {
        // https://github.com/Unity-Technologies/barracuda-release/blob/release/1.3.2/Barracuda/Runtime/Core/Model.cs
        reader = new barracuda.BinaryReader(reader);
        this.version = reader.int32();
        reader.int32();
        this.inputs = new Array(reader.int32());
        for (let i = 0; i < this.inputs.length; i++) {
            this.inputs[i] = {
                name: reader.string(),
                shape: reader.shape()
            };
        }
        this.outputs = reader.strings();
        this.memories = new Array(reader.int32());
        for (let i = 0; i < this.memories.length; i++) {
            this.memories[i] = {
                shape: reader.shape(),
                in: reader.string(),
                out: reader.string()
            };
        }
        this.layers = new Array(reader.int32());
        for (let i = 0; i < this.layers.length; i++) {
            const layer = {};
            layer.name = reader.string();
            layer.type = reader.int32();
            layer.activation = reader.int32();
            reader.int32();
            reader.int32();
            layer.pads = reader.int32s();
            layer.strides = reader.int32s();
            layer.pool_size = reader.int32s();
            layer.axis = reader.int32();
            layer.alpha = reader.float32();
            layer.beta = reader.float32();
            reader.int32();
            layer.inputs = reader.strings();
            layer.tensors = [];
            const tensorsLength = reader.int32();
            for (let j = 0; j < tensorsLength; j++) {
                layer.tensors.push({
                    name: reader.string(),
                    shape: reader.shape(),
                    offset: reader.int64().toNumber(),
                    itemsize: reader.int32(),
                    length: reader.int32()
                });
            }
            this.layers[i] = layer;
        }
        const position = reader.position;
        for (const layer of this.layers) {
            for (const tensor of layer.tensors) {
                const offset = tensor.offset;
                reader.seek(position + (offset * tensor.itemsize));
                tensor.data = reader.read(tensor.length * tensor.itemsize);
            }
        }
    }
};

barracuda.Activation = {
    0: "Linear", 1: "Relu", 2: "Softmax", 3: "Tanh", 4: "Sigmoid", 5: "Elu", 6: "Relu6", 7: "LeakyRelu", 8: "Selu", 9: "Swish",
    10: "LogSoftmax", 11: "Softplus", 12: "Softsign", 13: "PRelu",
    20: "Hardmax", 21: "HardSigmoid",
    100: "Abs", 101: "Neg", 102: "Ceil", 103: "Clip", 104: "Floor", 105: "Round",
    110: "Reciprocal", 111: "Sqrt", 113: "Exp", 114: "Log",
    200: "Acos", 201: "Acosh", 202: "Asin", 203: "Asinh", 204: "Atan", 205: "Atanh", 206: "Cos", 207: "Cosh", 208: "Sin", 209: "Sinh", 210: "Tan"
};

barracuda.BinaryReader = class {

    constructor(reader) {
        this._reader = reader;
    }

    get position() {
        return this._reader.position;
    }

    seek(position) {
        this._reader.seek(position);
    }

    skip(offset) {
        this._reader.skip(offset);
    }

    read(length) {
        return this._reader.read(length);
    }

    byte() {
        return this._reader.byte();
    }

    int32() {
        return this._reader.int32();
    }

    int32s() {
        const values = new Array(this.int32());
        for (let i = 0; i < values.length; i++) {
            values[i] = this.int32();
        }
        return values;
    }

    int64() {
        return this._reader.int64();
    }

    float32() {
        return this._reader.float32();
    }

    string() {
        let content = '';
        const size = this.int32();
        for (let i = 0; i < size; i++) {
            const c = this.byte();
            content += String.fromCharCode(c);
        }
        return content;
    }

    strings() {
        const values = [];
        const length = this.int32();
        for (let i = 0; i < length; i++) {
            values.push(this.string());
        }
        return values;
    }

    shape() {
        return this.int32s();
    }
};

barracuda.Metadata = class {

    static open() {
        barracuda.Metadata._metadata = barracuda.Metadata._metadata || new barracuda.Metadata();
        return barracuda.Metadata._metadata;
    }

    constructor() {
        this._types = new Map();
        const register = (id, name, category, inputs) => {
            this._types.set(id, { name, category, inputs: (inputs || []).map((input) => {
                return { name: input };
            }) });
        };
        register(0, 'Nop', '');
        register(1, 'Dense', 'Layer', ['input', 'kernel', 'bias']);
        register(2, 'MatMul', '', ['input', 'kernel', 'bias']);
        register(20, 'Conv2D', 'Layer', ['input', 'kernel', 'bias']);
        register(21, 'DepthwiseConv2D', 'Layer', ['input', 'kernel', 'bias']);
        register(22, 'Conv2DTrans', 'Layer', ['input', 'kernel', 'bias']);
        register(23, 'Upsample2D', 'Data');
        register(25, 'MaxPool2D', 'Pool');
        register(26, 'AvgPool2D', 'Pool');
        register(27, 'GlobalMaxPool2D', 'Pool');
        register(28, 'GlobalAvgPool2D', 'Pool');
        register(29, 'Border2D', '');
        register(30, 'Conv3D', 'Layer');
        register(32, 'Conv3DTrans', 'Layer');
        register(33, 'Upsample3D', 'Data');
        register(35, 'MaxPool3D', 'Pool');
        register(36, 'AvgPool3D', 'Pool');
        register(37, 'GlobalMaxPool3D', 'Pool');
        register(38, 'GlobalAvgPool3D', 'Pool');
        register(39, 'Border3D', '');
        register(50, 'Activation', '', ['input']);
        register(51, 'ScaleBias', 'Normalization', ['input', 'scale', 'bias']);
        register(52, 'Normalization', 'Normalization');
        register(53, 'LRN', 'Normalization');
        register(60, 'Dropout', 'Dropout');
        register(64, 'RandomNormal', '');
        register(65, 'RandomUniform', '');
        register(66, 'Multinomial', '');
        register(67, 'OneHot', '');
        register(68, 'TopKIndices', '');
        register(69, 'TopKValues', '');
        register(100, 'Add', '', ['inputs']);
        register(101, 'Sub', '', ['inputs']);
        register(102, 'Mul', '', ['inputs']);
        register(103, 'RealDiv', '', ['inputs']);
        register(104, 'Pow', '', ['inputs']);
        register(110, 'Minimum', '', ['inputs']);
        register(111, 'Maximum', '', ['inputs']);
        register(112, 'Mean', '', ['inputs']);
        register(120, 'ReduceL1', '', ['inputs']);
        register(121, 'ReduceL2', '', ['inputs']);
        register(122, 'ReduceLogSum', '', ['inputs']);
        register(123, 'ReduceLogSumExp', '', ['inputs']);
        register(124, 'ReduceMax', '', ['inputs']);
        register(125, 'ReduceMean', '', ['inputs']);
        register(126, 'ReduceMin', '', ['inputs']);
        register(127, 'ReduceProd', '', ['inputs']);
        register(128, 'ReduceSum', '', ['inputs']);
        register(129, 'ReduceSumSquare', '', ['inputs']);
        register(140, 'Greater', '');
        register(141, 'GreaterEqual', '');
        register(142, 'Less', '');
        register(143, 'LessEqual', '');
        register(144, 'Equal', '');
        register(145, 'LogicalOr', '');
        register(146, 'LogicalAnd', '');
        register(147, 'LogicalNot', '');
        register(148, 'LogicalXor', '');
        register(160, 'Pad2DReflect', '');
        register(161, 'Pad2DSymmetric', '');
        register(162, 'Pad2DEdge', '');
        register(200, 'Flatten', 'Shape');
        register(201, 'Reshape', 'Shape');
        register(202, 'Transpose', '');
        register(203, 'Squeeze', '');
        register(204, 'Unsqueeze', '');
        register(205, 'Gather', '');
        register(206, 'DepthToSpace', '');
        register(207, 'SpaceToDepth', '');
        register(208, 'Expand', '');
        register(209, 'Resample2D', '');
        register(210, 'Concat', 'Tensor', ['inputs']);
        register(211, 'StridedSlice', 'Shape');
        register(212, 'Tile', '');
        register(213, 'Shape', '');
        register(214, 'NonMaxSuppression', '');
        register(215, 'LSTM', '');
        register(255, 'Load', '');
    }

    type(name) {
        if (!this._types.has(name)) {
            this._types.set(name, { name: name.toString() });
        }
        return this._types.get(name);
    }
};

barracuda.Error = class extends Error {

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

export const ModelFactory = barracuda.ModelFactory;

