
// Experimental

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

const numpy = {};

numpy.ModelFactory = class {

    async match(context) {
        const stream = context.stream;
        const signature = [0x93, 0x4E, 0x55, 0x4D, 0x50, 0x59];
        if (stream && signature.length <= stream.length && stream.peek(signature.length).every((value, index) => value === signature[index])) {
            return context.set('npy');
        }
        const entries = await context.peek('npz');
        if (entries instanceof Map && entries.size > 0) {
            return context.set('npz', entries);
        }
        return null;
    }

    async open(context) {
        let format = '';
        const modules = [];
        switch (context.type) {
            case 'npy': {
                format = 'NumPy Array';
                const unresolved = new Set();
                const execution = new python.Execution();
                execution.on('resolve', (sender, name) => unresolved.add(name));
                const stream = context.stream;
                const io = execution.__import__('io');
                const np = execution.__import__('numpy');
                const bytes = new io.BytesIO(stream);
                const array = np.load(bytes);
                if (unresolved.size > 0) {
                    const name = unresolved.values().next().value;
                    throw new numpy.Error(`Unknown type name '${name}'.`);
                }
                const layer = { type: 'numpy.ndarray', parameters: [{ name: 'value', tensor: { name: '', array } }] };
                modules.push({ layers: [layer] });
                break;
            }
            case 'npz': {
                format = 'NumPy Archive';
                const layers = new Map();
                const entries = Array.from(context.value);
                const separator = entries.every(([name]) => name.endsWith('.weight.npy')) ? '.' : '/';
                for (const [key, array] of entries) {
                    const name = key.replace(/\.npy$/, '');
                    const path = name.split(separator);
                    const parameterName = path.pop();
                    const groupName = path.join(separator);
                    if (!layers.has(groupName)) {
                        layers.set(groupName, { name: groupName, parameters: [] });
                    }
                    const layer = layers.get(groupName);
                    layer.parameters.push({
                        name: parameterName,
                        tensor: { name, array }
                    });
                }
                modules.push({ layers: Array.from(layers.values()) });
                break;
            }
            default: {
                throw new numpy.Error(`Unsupported NumPy format '${context.type}'.`);
            }
        }
        return new numpy.Model(format, modules);
    }
};

numpy.Model = class {

    constructor(format, modules) {
        this.format = format;
        this.modules = modules.map((module) => new numpy.Module(module));
    }
};

numpy.Module = class {

    constructor(graph) {
        this.name = graph.name || '';
        this.nodes = graph.layers.map((layer) => new numpy.Node(layer));
        this.inputs = [];
        this.outputs = [];
    }
};

numpy.Argument = class {

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

numpy.Value = class {

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

numpy.Node = class {

    constructor(layer) {
        this.name = layer.name || '';
        this.type = { name: layer.type || 'Object' };
        this.inputs = [];
        this.outputs = [];
        this.attributes = [];
        for (const parameter of layer.parameters) {
            const initializer = new numpy.Tensor(parameter.tensor.array);
            const value = new numpy.Value(parameter.tensor.name || '', initializer);
            const argument = new numpy.Argument(parameter.name, [value]);
            this.inputs.push(argument);
        }
    }
};

numpy.Tensor = class  {

    constructor(array) {
        this.type = new numpy.TensorType(array.dtype.__name__, new numpy.TensorShape(array.shape));
        this.stride = array.strides.map((stride) => stride / array.itemsize);
        const list = this.type.dataType === 'string' || this.type.dataType === 'object' || this.type.dataType === 'void';
        this.values = list ? array.flatten().tolist() : array.tobytes();
        this.encoding = list ? '|' : array.dtype.byteorder;
    }
};

numpy.TensorType = class {

    constructor(dataType, shape) {
        this.dataType = dataType || '?';
        this.shape = shape;
    }

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

numpy.TensorShape = class {

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

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

numpy.Utility = class {

    static isTensor(obj) {
        return obj && obj.__class__ &&
            ((obj.__class__.__module__ === 'numpy' && obj.__class__.__name__ === 'ndarray') ||
             (obj.__class__.__module__ === 'numpy.core.memmap' && obj.__class__.__name__ === 'memmap'));
    }

    static weights(obj) {
        const dict = (obj, key) => {
            const dict = key === '' ? obj : obj[key];
            if (dict) {
                const weights = new Map();
                if (dict instanceof Map) {
                    for (const [key, obj] of dict) {
                        if (numpy.Utility.isTensor(obj)) {
                            weights.set(key, obj);
                            continue;
                        } else if (obj instanceof Map && Array.from(obj).every(([, value]) => numpy.Utility.isTensor(value))) {
                            for (const [name, value] of obj) {
                                weights.set(`${key}.${name}`, value);
                            }
                            continue;
                        } else if (key === '_metadata') {
                            continue;
                        }
                        return null;
                    }
                    return weights;
                } else if (!Array.isArray(dict)) {
                    const set = new Set(['weight_order', 'lr', 'model_iter', '__class__']);
                    for (const [name, value] of Object.entries(dict)) {
                        if (numpy.Utility.isTensor(value)) {
                            weights.set(name, value);
                            continue;
                        }
                        if (set.has(name)) {
                            continue;
                        }
                        if (value && !Array.isArray(value) && Object.entries(value).every(([, value]) => numpy.Utility.isTensor(value))) {
                            if (value && value.__class__ && value.__class__.__module__ && value.__class__.__name__) {
                                weights.set(`${name}.__class__`, `${value.__class__.__module__}.${value.__class__.__name__}`);
                            }
                            for (const [key, obj] of Object.entries(value)) {
                                weights.set(`${name}.${key}`, obj);
                            }
                            continue;
                        }
                        return null;
                    }
                    return weights;
                }
            }
            return null;
        };
        const list = (obj, key) => {
            let list = key === '' ? obj : obj[key];
            if (list && Array.isArray(list) && list.every((obj) => Object.values(obj).every((value) => numpy.Utility.isTensor(value)))) {
                list = list.map((obj) => obj instanceof Map ? obj : new Map(Object.entries(obj)));
            }
            if (list && Array.isArray(list)) {
                const weights = new Map();
                for (let i = 0; i < list.length; i++) {
                    const obj = list[i];
                    if (numpy.Utility.isTensor(obj)) {
                        weights.set(i.toString(), obj);
                        continue;
                    } else if (obj instanceof Map && Array.from(obj).every(([, value]) => numpy.Utility.isTensor(value))) {
                        for (const [name, value] of obj) {
                            weights.set(`${i}.${name}`, value);
                        }
                        continue;
                    }
                    return null;
                }
                return weights;
            }
            return null;
        };
        const keys = ['', 'blobs', 'model', 'experiment_state'];
        for (const key of keys) {
            const weights = dict(obj, key);
            if (weights && weights.size > 0) {
                return weights;
            }
        }
        for (const key of keys) {
            const weights = list(obj, key);
            if (weights) {
                return weights;
            }
        }
        return null;
    }
};

numpy.Error = class extends Error {

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

export const ModelFactory = numpy.ModelFactory;
