
// Experimental

const sklearn = {};

sklearn.ModelFactory = class {

    async match(context) {
        const obj = await context.peek('pkl');
        const validate = (obj, name) => {
            if (obj && obj.__class__ && obj.__class__.__module__ && obj.__class__.__name__) {
                const key = `${obj.__class__.__module__}.${obj.__class__.__name__}`;
                return key.startsWith(name);
            }
            return false;
        };
        const formats = [
            { name: 'sklearn.', format: 'sklearn' },
            { name: 'xgboost.sklearn.', format: 'sklearn' },
            { name: 'lightgbm.sklearn.', format: 'sklearn' },
            { name: 'scipy.', format: 'scipy' },
            { name: 'hmmlearn.', format: 'hmmlearn' }
        ];
        for (const format of formats) {
            if (validate(obj, format.name)) {
                return context.set(format.format, obj);
            }
            if (Array.isArray(obj) && obj.length > 0 && obj.every((item) => validate(item, format.name))) {
                return context.set(`${format.format}.list`, obj);
            }
            if (Object(obj) === obj || obj instanceof Map) {
                const entries = obj instanceof Map ? Array.from(obj) : Object.entries(obj);
                if (entries.length > 0 && entries.every(([, value]) => validate(value, format.name))) {
                    return context.set(`${format.format}.map`, obj);
                }
            }
        }
        return null;
    }

    async open(context) {
        const metadata = await context.metadata('sklearn-metadata.json');
        return new sklearn.Model(metadata, context.type, context.value);
    }
};

sklearn.Model = class {

    constructor(metadata, type, obj) {
        const formats = new Map([
            ['sklearn', 'scikit-learn'],
            ['scipy', 'SciPy'],
            ['hmmlearn', 'hmmlearn']
        ]);
        this.format = formats.get(type.split('.').shift());
        this.modules = [];
        const version = [];
        switch (type) {
            case 'sklearn':
            case 'scipy':
            case 'hmmlearn': {
                if (obj._sklearn_version) {
                    version.push(` v${obj._sklearn_version}`);
                }
                this.modules.push(new sklearn.Module(metadata, '', obj));
                break;
            }
            case 'sklearn.list':
            case 'scipy.list': {
                const list = obj;
                for (let i = 0; i < list.length; i++) {
                    const obj = list[i];
                    this.modules.push(new sklearn.Module(metadata, i.toString(), obj));
                    if (obj._sklearn_version) {
                        version.push(` v${obj._sklearn_version}`);
                    }
                }
                break;
            }
            case 'sklearn.map':
            case 'scipy.map': {
                const entries = obj instanceof Map ? Array.from(obj) : Object.entries(obj);
                for (const [name, value] of entries) {
                    this.modules.push(new sklearn.Module(metadata, name, value));
                    if (value._sklearn_version) {
                        version.push(` v${value._sklearn_version}`);
                    }
                }
                break;
            }
            default: {
                throw new sklearn.Error(`Unsupported scikit-learn format '${type}'.`);
            }
        }
        if (version.length > 0 && version.every((value) => value === version[0])) {
            this.format += version[0];
        }
    }
};

sklearn.Module = class {

    constructor(metadata, name = '', obj = null) {
        this.name = name;
        this.nodes = [];
        this.inputs = [];
        this.outputs = [];
        const node = new sklearn.Node(metadata, '', obj);
        this.nodes.push(node);
    }
};

sklearn.Argument = class {

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

sklearn.Value = class {

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

sklearn.Node = class {

    constructor(metadata, name, obj, stack) {
        this.name = name || '';
        const type = obj.__class__ ? `${obj.__class__.__module__}.${obj.__class__.__name__}` : 'builtins.dict';
        this.type = metadata.type(type) || { name: type };
        this.inputs = [];
        this.outputs = [];
        const isObject = (obj) => {
            if (obj && typeof obj === 'object') {
                const proto = Object.getPrototypeOf(obj);
                return proto === Object.prototype || proto === null;
            }
            return false;
        };
        if (type === 'builtins.bytearray') {
            const attribute = new sklearn.Argument('value', Array.from(obj), 'byte[]');
            this.inputs.push(attribute);
        } else {
            const entries = Object.entries(obj);
            for (const [name, value] of entries) {
                if (name === '__class__') {
                    continue;
                } else if (value && sklearn.Utility.isTensor(value)) {
                    const tensor = new sklearn.Tensor(value);
                    const argument = new sklearn.Argument(name, tensor, 'tensor');
                    this.inputs.push(argument);
                } else if (Array.isArray(value) && value.length > 0 && value.every((obj) => sklearn.Utility.isTensor(obj))) {
                    const tensors = value.map((obj) => new sklearn.Tensor(obj));
                    const argument = new sklearn.Argument(name, tensors, 'tensor[]');
                    this.inputs.push(argument);
                } else if (sklearn.Utility.isType(value, 'builtins.bytearray')) {
                    const argument = new sklearn.Argument(name, Array.from(value), 'byte[]');
                    this.inputs.push(argument);
                } else {
                    stack = stack || new Set();
                    if (value && Array.isArray(value) && value.every((obj) => typeof obj === 'string')) {
                        const argument = new sklearn.Argument(name, value, 'string[]');
                        this.inputs.push(argument);
                    } else if (value && Array.isArray(value) && value.every((obj) => typeof obj === 'number')) {
                        const argument = new sklearn.Argument(name, value, 'attribute');
                        this.inputs.push(argument);
                    } else if (sklearn.Utility.isType(value, 'builtins.function') || sklearn.Utility.isType(value, 'builtins.type')) {
                        const node = new sklearn.Node(metadata, '', { __class__: value }, stack);
                        const argument = new sklearn.Argument(name, node, 'object');
                        this.inputs.push(argument);
                    } else if (sklearn.Utility.isType(value, 'builtins.list') && value.every((value) => Array.isArray(value) && value.length === 2 && typeof value[0] === 'string')) {
                        const chain = stack;
                        const nodes = value.map(([name, value]) => {
                            chain.add(value);
                            const node = new sklearn.Node(metadata, name, value, chain);
                            chain.delete(value);
                            return node;
                        });
                        const argument = new sklearn.Argument(name, nodes, 'object[]');
                        this.inputs.push(argument);
                    } else if (value && Array.isArray(value) && value.length > 0 && value.every((obj) => obj && (obj.__class__ || obj === Object(obj)))) {
                        const chain = stack;
                        const values = value.filter((value) => !chain.has(value));
                        const nodes = values.map((value) => {
                            chain.add(value);
                            const node = new sklearn.Node(metadata, '', value, null, chain);
                            chain.delete(value);
                            return node;
                        });
                        const argument = new sklearn.Argument(name, nodes, 'object[]');
                        this.inputs.push(argument);
                    } else if (value && (value.__class__ || isObject(value)) && !stack.has(value)) {
                        stack.add(value);
                        const node = new sklearn.Node(metadata, '', value, null, stack);
                        const argument = new sklearn.Argument(name, node, 'object');
                        this.inputs.push(argument);
                        stack.delete(value);
                    } else {
                        let type = 'attribute';
                        let visible = true;
                        const schema = metadata.attribute(type, name);
                        if (schema) {
                            if (schema.type) {
                                type = schema.type;
                            }
                            if (schema.visible === false || (schema.optional && value === null)) {
                                visible = false;
                            } else if (schema.default !== undefined) {
                                if (Array.isArray(value)) {
                                    if (Array.isArray(schema.default)) {
                                        visible = value.length !== schema.default || !value.every((item, index) => item === metadata.default[index]);
                                    } else {
                                        visible = !value.every((item) => item === schema.default);
                                    }
                                } else {
                                    visible = value !== schema.default;
                                }
                            }
                        }
                        const argument = new sklearn.Argument(name, value, type, visible);
                        this.inputs.push(argument);
                    }
                }
            }
        }
    }
};

sklearn.Tensor = class {

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

sklearn.TensorType = class {

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

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

sklearn.TensorShape = class {

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

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

sklearn.Utility = class {

    static isType(obj, name) {
        return obj && obj.__class__ && obj.__class__.__module__ && obj.__class__.__name__ && `${obj.__class__.__module__}.${obj.__class__.__name__}` === name;
    }

    static isTensor = (obj) => {
        return sklearn.Utility.isType(obj, 'numpy.ndarray') || sklearn.Utility.isType(obj, 'numpy.matrix');
    };
};

sklearn.Error = class extends Error {

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

export const ModelFactory = sklearn.ModelFactory;
