/*jshint esversion: 6 */

var coreml = null;

class CoreMLModelFactory {

    match(buffer, identifier) {
        var extension = identifier.split('.').pop();
        return extension == 'mlmodel';
    }

    open(buffer, identifier, host, callback) { 
        host.import('/coreml.js', (err) => {
            if (err) {
                callback(err, null);
                return;
            }
            var decodedBuffer = null;
            try {
                coreml = protobuf.roots.coreml.CoreML.Specification;
                decodedBuffer = coreml.Model.decode(buffer);
            }
            catch (error) {
                callback(new CoreMLError('Protocol Buffer loader failed to decode coreml.Model input stream (' + error.message + ').'), null);
                return;
            }
            var model = null;
            try {
                model = new CoreMLModel(decodedBuffer);
            }
            catch (error) {
                callback(new CoreMLError(error.message), null);
                return;
            }
            CoreMLOperatorMetadata.open(host, (err, metadata) => {
                callback(null, model);
            });
        });
    }
}

class CoreMLModel {

    constructor(model) {
        this._specificationVersion = model.specificationVersion;
        this._description = model.description;
        this._graphs = [ new CoreMLGraph(model) ];
    }

    get properties() {
        var results = [];

        results.push({ name: 'Format', value: 'CoreML v' + this._specificationVersion.toString() });

        if (this._description && this._description.metadata) {
            var metadata = this._description.metadata;
            if (metadata.versionString) {
                results.push({ name: 'Version', value: metadata.versionString });
            }
            if (metadata.author) {
                results.push({ name: 'Author', value: metadata.author });
            }
            if (metadata.shortDescription) {
                results.push({ name: 'Description', value: metadata.shortDescription });
            }
            if (metadata.license) {
                results.push({ name: 'License', value: metadata.license });
            }
            if (metadata.userDefined && Object.keys(metadata.userDefined).length > 0) {
                debugger;
            }
        }

        return results;
    }

    get graphs() {
        return this._graphs;
    }
}

class CoreMLGraph {

    constructor(model)
    {
        this._description = model.description;
        this._groups = false; 

        this._inputs = this._description.input.map((input) => {
            return {
                id: input.name,
                name: input.name,
                description: input.shortDescription,
                type: CoreMLGraph.formatFeatureType(input.type) 
            };
        });

        this._outputs = this._description.output.map((output) => {
            return {
                id: output.name,
                name: output.name,
                description: output.shortDescription,
                type: CoreMLGraph.formatFeatureType(output.type) 
            };
        });

        this._nodes = [];
        this._type = this.loadModel(model, {}, '');
    }

    get name() {
        return '';
    }

    get type() {
        return this._type;
    }

    get inputs() {
        return this._inputs;
    }

    get outputs() {
        return this._outputs;
    }

    get nodes() {
        return this._nodes;
    }

    get groups() {
        return this._groups;
    }

    updateOutput(name, newName) {
        this._nodes.forEach((node) => {
            node._outputs = node._outputs.map((output) => (output != name) ? output : newName);
        });
        return newName;
    }

    updateClassifierOutput(group, classifier) {
        var labelProbabilityLayerName = classifier.labelProbabilityLayerName;
        if (!labelProbabilityLayerName && this._nodes.length > 0) {
            labelProbabilityLayerName = this._nodes.slice(-1).pop()._outputs[0];
        }
        var predictedFeatureName = this._description.predictedFeatureName;
        var predictedProbabilitiesName = this._description.predictedProbabilitiesName;
        if ((predictedFeatureName || predictedProbabilitiesName) && labelProbabilityLayerName && classifier.ClassLabels) {
            predictedFeatureName = predictedFeatureName ? predictedFeatureName : '?';
            predictedProbabilitiesName = predictedProbabilitiesName ? predictedProbabilitiesName : '?';
            var labelProbabilityInput = this.updateOutput(labelProbabilityLayerName, labelProbabilityLayerName + ':labelProbabilityLayerName');
            var operator = classifier.ClassLabels;
            this._nodes.push(new CoreMLNode(group, operator, null, classifier[operator], [ labelProbabilityInput ], [ predictedProbabilitiesName, predictedFeatureName ]));
        }
    }

    loadModel(model, scope, group) {
        this._groups = this._groups | (group.length > 0 ? true : false);
        if (model.neuralNetworkClassifier) {
            var neuralNetworkClassifier = model.neuralNetworkClassifier;
            neuralNetworkClassifier.layers.forEach((layer) => {
                var operator = layer.layer;
                this.createNode(scope, group, operator, layer.name, layer[operator], layer.input, layer.output);
            });
            this.updateClassifierOutput(group, neuralNetworkClassifier);
            if (neuralNetworkClassifier.preprocessing && neuralNetworkClassifier.preprocessing.length > 0) {               
                var preprocessingInput = this._description.input[0].name;
                var inputNodes = [];
                this._nodes.forEach((node) => {
                    if (node._inputs.some((input => input == preprocessingInput))) {
                        inputNodes.push(node);
                    }
                });
                var preprocessorOutput = preprocessingInput;
                var preprocessorIndex = 0;
                var nodes = [];
                neuralNetworkClassifier.preprocessing.forEach((preprocessing) => {
                    var operator = preprocessing.preprocessor;
                    var input = preprocessing.featureName ? preprocessing.featureName : preprocessorOutput;
                    preprocessorOutput = preprocessingInput + ':' + preprocessorIndex.toString();
                    this.createNode(scope, group, operator, null, preprocessing[operator], [ input ], [ preprocessorOutput ]);
                    preprocessorIndex++;
                });
                inputNodes.forEach((node) => {
                    node._inputs = node._inputs.map((input) => (input != preprocessingInput) ? input : preprocessorOutput);
                });
            }
            return 'Neural Network Classifier';
        }
        else if (model.neuralNetwork) {
            model.neuralNetwork.layers.forEach((layer) => {
                var operator = layer.layer;
                this.createNode(scope, group, operator, layer.name, layer[operator], layer.input, layer.output);
            });
            return 'Neural Network';
        }
        else if (model.neuralNetworkRegressor) {
            model.neuralNetworkRegressor.layers.forEach((layer) => {
                var operator = layer.layer;
                this.createNode(scope, group, operator, layer.name, layer[operator], layer.input, layer.output);
            });
            return 'Neural Network Regressor';
        }
        else if (model.pipeline) {
            model.pipeline.models.forEach((subModel) => {
                this.loadModel(subModel, scope, (group ? (group + '/') : '') + 'pipeline');
            });
            return 'Pipeline';
        }
        else if (model.pipelineClassifier) {
            model.pipelineClassifier.pipeline.models.forEach((subModel) => {
                this.loadModel(subModel, scope, (group ? (group + '/') : '') + 'pipelineClassifier');
            });
            return 'Pipeline Classifier';
        }
        else if (model.pipelineRegressor) {
            model.pipelineRegressor.pipeline.models.forEach((subModel) => {
                this.loadModel(subModel, scope, (group ? (group + '/') : '') + 'pipelineRegressor');
            });
            return 'Pipeline Regressor';
        }
        else if (model.glmClassifier) {
            this.createNode(scope, group, 'glmClassifier', null, 
                { classEncoding: model.glmClassifier.classEncoding, 
                  offset: model.glmClassifier.offset, 
                  weights: model.glmClassifier.weights }, 
                [ model.description.input[0].name ],
                [ model.description.predictedProbabilitiesName ]);
            this.updateClassifierOutput(group, model.glmClassifier);
            return 'Generalized Linear Classifier';
        }
        else if (model.glmRegressor) {
            this.createNode(scope, group, 'glmRegressor', null, 
                model.glmRegressor,
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Generalized Linear Regressor';
        }
        else if (model.dictVectorizer) {
            this.createNode(scope, group, 'dictVectorizer', null, model.dictVectorizer,
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Dictionary Vectorizer';
        }
        else if (model.featureVectorizer) {
            this.createNode(scope, group, 'featureVectorizer', null, model.featureVectorizer, 
            CoreMLGraph.formatFeatureDescriptionList(model.description.input),
            [ model.description.output[0].name ]);
            return 'Feature Vectorizer';
        }
        else if (model.treeEnsembleClassifier) {
            this.createNode(scope, group, 'treeEnsembleClassifier', null, model.treeEnsembleClassifier.treeEnsemble, 
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            this.updateClassifierOutput(group, model.treeEnsembleClassifier);
            return 'Tree Ensemble Classifier';
        }
        else if (model.treeEnsembleRegressor) {
            this.createNode(scope, group, 'treeEnsembleRegressor', null, model.treeEnsembleRegressor.treeEnsemble, 
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Tree Ensemble Regressor';
        }
        else if (model.supportVectorClassifier) {
            this.createNode(scope, group, 'supportVectorClassifier', null, 
                { coefficients: model.supportVectorClassifier.coefficients, 
                  denseSupportVectors: model.supportVectorClassifier.denseSupportVectors,
                  kernel: model.supportVectorClassifier.kernel,
                  numberOfSupportVectorsPerClass: model.supportVectorClassifier.numberOfSupportVectorsPerClass,
                  probA: model.supportVectorClassifier.probA,
                  probB: model.supportVectorClassifier.probB,
                  rho: model.supportVectorClassifier.rho,
                  supportVectors: model.supportVectorClassifier.supportVectors }, 
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            this.updateClassifierOutput(group, model.supportVectorClassifier);
            return 'Support Vector Classifier';            
        }
        else if (model.supportVectorRegressor) {
            this.createNode(scope, group, 'supportVectorRegressor', null, 
                { coefficients: model.supportVectorRegressor.coefficients, 
                  kernel: model.supportVectorRegressor.kernel,
                  rho: model.supportVectorRegressor.rho,
                  supportVectors: model.supportVectorRegressor.supportVectors }, 
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Support Vector Regressor';
        }
        else if (model.arrayFeatureExtractor) {
            this.createNode(scope, group, 'arrayFeatureExtractor', null, 
                { extractIndex: model.arrayFeatureExtractor.extractIndex },
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Array Feature Extractor';
        }
        else if (model.oneHotEncoder) {
            var categoryType = model.oneHotEncoder.CategoryType;
            var oneHotEncoderParams = { outputSparse: model.oneHotEncoder.outputSparse };
            oneHotEncoderParams[categoryType] = model.oneHotEncoder[categoryType];
            this.createNode(scope, group, 'oneHotEncoder', null, 
                oneHotEncoderParams,
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'One Hot Encoder';
        }
        else if (model.imputer) {
            var imputedValue = model.imputer.ImputedValue;
            var replaceValue = model.imputer.ReplaceValue;
            var imputerParams = {};
            imputerParams[imputedValue] = model.imputer[imputedValue];
            imputerParams[replaceValue] = model.imputer[replaceValue];
            this.createNode(scope, group, 'oneHotEncoder', null, 
                imputerParams,
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
            return 'Imputer';
            
        }
        else if (model.normalizer) {
            this.createNode(scope, group, 'normalizer', null, 
                model.normalizer,
                [ model.description.input[0].name ],
                [ model.description.output[0].name ]);
        }
        return 'Unknown';
    }

    createNode(scope, group, operator, name, data, inputs, outputs) {
        inputs = inputs.map((input) => scope[input] ? scope[input].connection : input);
        outputs = outputs.map((output) => {
            if (scope[output]) {
                scope[output].counter++;
                var next = output + '@' + scope[output].counter.toString();
                scope[output].connection = next;
                return next;
            }
            scope[output] = {
                connection: output,
                counter: 0
            };
            return output;
        });

        var node = new CoreMLNode(group, operator, name, data, inputs, outputs);
        this._nodes.push(node);
        return node;
    }

    static formatFeatureType(type) {
        var result = '';
        switch (type.Type) {
            case 'multiArrayType':
                result = CoreMLGraph.formatArrayDataType(type.multiArrayType.dataType);
                if (type.multiArrayType.shape && type.multiArrayType.shape.length > 0) {
                    result += '[' + type.multiArrayType.shape.map(dimension => dimension.toString()).join(',') + ']';
                }
                break;
            case 'imageType':
                result = 'image(' + CoreMLGraph.formatColorSpace(type.imageType.colorSpace) + ',' + type.imageType.width.toString() + 'x' + type.imageType.height.toString() + ')';
                break;
            case 'dictionaryType':
                result = 'map<' + type.dictionaryType.KeyType.replace('KeyType', '') + ',double>';
                break;
            case 'stringType':
                result = 'string';
                break;
            case 'doubleType':
                result = 'double';
                break;
            case 'int64Type':
                result = 'int64';
                break;
        }
        if (type.isOptional) {
            result += '?';
        }
        return result;
    }

    static formatArrayDataType(dataType) {
        switch (dataType) {
            case coreml.ArrayFeatureType.ArrayDataType.FLOAT32:
                return 'float32';
            case coreml.ArrayFeatureType.ArrayDataType.INT32:
                return 'int32';
            case coreml.ArrayFeatureType.ArrayDataType.DOUBLE:
                return 'double';
        }
        return '?';
    }

    static formatColorSpace(colorSpace) {
        switch (colorSpace) {
            case coreml.ImageFeatureType.ColorSpace.GRAYSCALE:
                return 'Grayscale';
            case coreml.ImageFeatureType.ColorSpace.RGB:
                return 'RGB';
            case coreml.ImageFeatureType.ColorSpace.BGR:
                return 'BGR';
        }
        return '?';
    }

    static formatFeatureDescriptionList(list) {
        return list.map((item) => item.name);
    }
}

class CoreMLNode {

    constructor(group, operator, name, data, inputs, outputs) {
        if (group) {
            this._group = group;
        }
        this._operator = operator;
        this._name = name;
        this._inputs = inputs;
        this._outputs = outputs;
        this._attributes = [];
        this._initializers = [];
        if (data) {
            var initializerMap = this.initializer(data);
            Object.keys(data).forEach((key) => {
                if (!initializerMap[key]) {
                    this._attributes.push(new CoreMLAttribute(this, key, data[key]));
                }
            });
        }
    }

    get operator() {
        return this._operator;
    }

    get name() {
        return this._name;
    }

    get category() {
        return CoreMLOperatorMetadata.operatorMetadata.getOperatorCategory(this.operator);
    }

    get group() {
        return this._group ? this._group : null;
    }

    get documentation() {
        return CoreMLOperatorMetadata.operatorMetadata.getOperatorDocumentation(this.operator);
    }

    get inputs() {
        var results = [];
        CoreMLOperatorMetadata.operatorMetadata.getInputs(this._operator, this._inputs).forEach((input) => {
            results.push(input);
        });
        this._initializers.forEach((initializer) => {
            var input = {
                name: initializer.name,
                connections: [ { 
                    id: '',
                    type: initializer.type,
                    initializer: initializer, } ]
            };
            if (!CoreMLOperatorMetadata.operatorMetadata.getInputVisible(this._operator, initializer.name)) {
                input.hidden = true;
            }
            results.push(input);
        });
        return results;
    }

    get outputs() {
        var results = [];
        this._outputs.forEach((output, index) => {
            results.push({
                name: CoreMLOperatorMetadata.operatorMetadata.getOutputName(this._operator, index),
                connections: [ { id: output } ]
            });
        });
        return results;
    }

    get attributes() {
        return this._attributes;
    }

    initializer(data) {
        switch (this._operator) {
            case 'convolution':
                var weightsShape = [ data.outputChannels, data.kernelChannels, data.kernelSize[0], data.kernelSize[1] ];
                if (data.isDeconvolution) {
                    weightsShape[0] = data.kernelChannels;
                    weightsShape[1] = Math.floor(data.outputChannels / (data.nGroups != 0 ? data.nGroups : 1));
                }    
                this._initializers.push(new CoreMLTensor('Weights', 'weights', weightsShape, data.weights));
                if (data.hasBias) {
                    this._initializers.push(new CoreMLTensor('Weights', 'bias', [ data.bias.floatValue.length ], data.bias));
                }
                return { 'weights': true, 'bias': data.hasBias };
            case 'innerProduct':
                this._initializers.push(new CoreMLTensor('Weights', 'weights', [ data.outputChannels, data.inputChannels ], data.weights));
                if (data.hasBias) {
                    this._initializers.push(new CoreMLTensor('Weights', 'bias', [ data.outputChannels ], data.bias));
                }
                return { 'weights': true, 'bias': data.hasBias };
            case 'batchnorm':
                this._initializers.push(new CoreMLTensor('Weights', 'gamma', [ data.channels ], data.gamma));
                this._initializers.push(new CoreMLTensor('Weights', 'beta', [ data.channels ], data.beta));
                if (data.mean) {
                    this._initializers.push(new CoreMLTensor('Weights', 'mean', [ data.channels ], data.mean));
                }
                if (data.variance) {
                    this._initializers.push(new CoreMLTensor('Weights', 'variance', [ data.channels ], data.variance));
                }
                return { 'gamma': true, 'beta': true, 'mean': true, 'variance': true };
            case 'embedding':
                this._initializers.push(new CoreMLTensor('Weights', 'weights', [ data.inputDim, data.outputChannels ], data.weights));
                return { 'weights': true };
            case 'loadConstant':    
                this._initializers.push(new CoreMLTensor('Weights', 'data', data.shape, data.data));            
                return { 'data': true };
            case 'scale':
                this._initializers.push(new CoreMLTensor('Weights', 'scale', data.shapeScale, data.scale));
                if (data.hasBias) {
                    this._initializers.push(new CoreMLTensor('Weights', 'bias', data.shapeBias, data.bias));
                }
                return { 'scale': true, 'bias': data.hasBias };
            case 'bias':
                this._initializers.push(new CoreMLTensor('Weights', 'bias', data.shapeBias, data.bias));
                return { 'bias': true };
            case 'simpleRecurrentLayer':
                this._initializers.push(new CoreMLTensor('Weights', 'weights', null, data.weightMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'recurrent', null, data.recursionMatrix));
                if (data.hasBiasVectors) {
                    this._initializers.push(new CoreMLTensor('Weights', 'bias', null, data.biasVector));
                }
                return { 'weightMatrix': true, 'recursionMatrix': true, 'biasVector': data.hasBiasVectors };
            case 'gru':
                this._initializers.push(new CoreMLTensor('Weights', 'updateGateWeightMatrix', null, data.updateGateWeightMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'resetGateWeightMatrix', null, data.resetGateWeightMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'outputGateWeightMatrix', null, data.outputGateWeightMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'updateGateRecursionMatrix', null, data.updateGateRecursionMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'resetGateRecursionMatrix', null, data.resetGateRecursionMatrix));
                this._initializers.push(new CoreMLTensor('Weights', 'outputGateRecursionMatrix', null, data.outputGateRecursionMatrix));
                if (data.hasBiasVectors) {
                    this._initializers.push(new CoreMLTensor('Weights', 'updateGateBiasVector', null, data.updateGateBiasVector));
                    this._initializers.push(new CoreMLTensor('Weights', 'resetGateBiasVector', null, data.resetGateBiasVector));
                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateBiasVector', null, data.outputGateBiasVector));
                }  
                return {
                    'updateGateWeightMatrix': true, 'resetGateWeightMatrix': true, 'outputGateWeightMatrix': true, 
                    'updateGateRecursionMatrix': true, 'resetGateRecursionMatrix': true, 'outputGateRecursionMatrix': true,
                    'updateGateBiasVector': data.hasBiasVectors, 'resetGateBiasVector': data.hasBiasVectors, 'outputGateBiasVector': data.hasBiasVectors };
            case 'uniDirectionalLSTM':
            case 'biDirectionalLSTM':
                var count = (this._operator == 'uniDirectionalLSTM') ? 1 : 2;
                var matrixShape = [ data.outputVectorSize, data.inputVectorSize ];
                var vectorShape = [ data.outputVectorSize ];
                for (var i = 0; i < count; i++) {
                    var weights = count == 1 ? data.weightParams : data.weightParams[i];
                    var suffix = (i == 0) ? '' : '_rev';
                    this._initializers.push(new CoreMLTensor('Weights', 'inputGateWeightMatrix' + suffix, matrixShape, weights.inputGateWeightMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'forgetGateWeightMatrix' + suffix, matrixShape, weights.forgetGateWeightMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'blockInputWeightMatrix' + suffix, matrixShape, weights.blockInputWeightMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateWeightMatrix' + suffix, matrixShape, weights.outputGateWeightMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'inputGateRecursionMatrix' + suffix, matrixShape, weights.inputGateRecursionMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'forgetGateRecursionMatrix' + suffix, matrixShape,weights.forgetGateRecursionMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'blockInputRecursionMatrix' + suffix, matrixShape, weights.blockInputRecursionMatrix));
                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateRecursionMatrix' + suffix, matrixShape, weights.outputGateRecursionMatrix));
                    if (data.params.hasBiasVectors) {
                        this._initializers.push(new CoreMLTensor('Weights', 'inputGateBiasVector' + suffix, vectorShape, weights.inputGateBiasVector));
                        this._initializers.push(new CoreMLTensor('Weights', 'forgetGateBiasVector' + suffix, vectorShape, weights.forgetGateBiasVector));
                        this._initializers.push(new CoreMLTensor('Weights', 'blockInputBiasVector' + suffix, vectorShape, weights.blockInputBiasVector));
                        this._initializers.push(new CoreMLTensor('Weights', 'outputGateBiasVector' + suffix, vectorShape, weights.outputGateBiasVector));
                    }
                    if (data.params.hasPeepholeVectors) {
                        this._initializers.push(new CoreMLTensor('Weights', 'inputGatePeepholeVector' + suffix, vectorShape, weights.inputGatePeepholeVector));
                        this._initializers.push(new CoreMLTensor('Weights', 'forgetGatePeepholeVector' + suffix, vectorShape, weights.forgetGatePeepholeVector));
                        this._initializers.push(new CoreMLTensor('Weights', 'outputGatePeepholeVector' + suffix, vectorShape, weights.outputGatePeepholeVector));
                    }
                }
                return { 'weightParams': true };
        }
        return {};
    }
}

class CoreMLAttribute {

    constructor(owner, name, value) {
        this._owner = owner;
        this._name = name;
        this._value = value;
    }

    get name() {
        return this._name;
    }

    get value() {
        if (Array.isArray(this._value)) {
            return this._value.map((item) => JSON.stringify(item)).join(', ');
        }
        if (Number.isNaN(this._value)) {
            return 'NaN';
        }
        return JSON.stringify(this._value);
    }

    get visible() {
        return CoreMLOperatorMetadata.operatorMetadata.getAttributeVisible(this._owner.operator, this._name);
    }
}

class CoreMLTensor {

    constructor(kind, name, shape, data) {
        this._kind = kind;
        this._name = name;
        this._shape = shape;
        this._type = null;
        this._data = null;
        if (data) {
            if (data.floatValue && data.floatValue.length > 0) {
                this._data = data.floatValue;
                this._type = 'float';
            }
            else if (data.float16Value && data.float16Value.length > 0) {
                this._data = data.float16Value;
                this._type = 'float16';
            }
            else if (data.rawValue && data.rawValue.length > 0) {
                this._data = null;
                this._type = 'byte';
                this._shape = [];
            }
        }
    }

    get id() {
        return this._name;
    }

    get name() {
        return this._name;
    }

    get kind() {
        return this._kind;
    }

    get type() {
        if (this._type && this._shape) {
            return this._type + '[' + this._shape.join(',') + ']';
        }
        return '?';
    }

    get value() {
        if (this._data) {
            this._index = 0;
            this._count = 0;
            var result = this.read(0);
            delete this._index;
            delete this._count;
            return JSON.stringify(result, null, 4);
        }
        return '?';
    }

    read(dimension) {
        var results = [];
        var size = this._shape[dimension];
        if (dimension == this._shape.length - 1) {
            for (var i = 0; i < size; i++) {
                if (this._count > 10000) {
                    results.push('...');
                    return results;
                }
                results.push(this._data[this._index]);
                this._index++;
                this._count++;
            }
        }
        else {
            for (var j = 0; j < size; j++) {
                if (this._count > 10000) {
                    results.push('...');
                    return results;
                }
                results.push(this.read(dimension + 1));
            }
        }
        return results;
    }
}

class CoreMLOperatorMetadata 
{

    static open(host, callback) {
        if (CoreMLOperatorMetadata.operatorMetadata) {
            callback(null, CoreMLOperatorMetadata.operatorMetadata);
        }
        else {
            host.request('/coreml-metadata.json', (err, data) => {
                CoreMLOperatorMetadata.operatorMetadata = new CoreMLOperatorMetadata(data);
                callback(null, CoreMLOperatorMetadata.operatorMetadata);
            });
        }    
    }

    constructor(data) {
        this._map = {};
        if (data) {
            var items = JSON.parse(data);
            if (items) {
                items.forEach((item) => {
                    if (item.name && item.schema)
                    {
                        var name = item.name;
                        var schema = item.schema;
                        this._map[name] = schema;
                    }
                });
            }
        }
    }

    getOperatorCategory(operator) {
        var schema = this._map[operator];
        if (schema && schema.category) {
            return schema.category;
        }
        return null;
    }

    getInputs(operator, inputs) {
        var results = [];
        var schema = this._map[operator];
        var index = 0;
        while (index < inputs.length) {
            var result = { connections: [] };
            var count = 1;
            var name = null;
            if (schema && schema.inputs) {
                if (index < schema.inputs.length) {
                    var input = schema.inputs[index];
                    name = input.name;
                    if (schema.inputs[index].option == 'variadic') {
                        count = inputs.length - index;
                    }
                }
            }
            else {
                if (index == 0) {
                    name = 'input';
                }
            }
            result.name = name ? name : '(' + index.toString() + ')';
            var array = inputs.slice(index, index + count);
            for (var j = 0; j < array.length; j++) {
                result.connections.push({ id: array[j] });
            }
            index += count;
            results.push(result);
        }
        return results;
    }

    getInputVisible(operator, name) {
        var schema = this._map[operator];
        if (schema && schema.inputs) {
            if (!schema.inputsMap) {
                schema.inputsMap = {};
                schema.inputs.forEach((input) => {
                    schema.inputsMap[input.name] = input;
                });
            }
            var input = schema.inputsMap[name];
            if (input && input.hasOwnProperty('visible')) {
                return input.visible;
            }
        }
        return true;
    }

    getOutputName(operator, index) {
        var schema = this._map[operator];
        if (schema) {
            var outputs = schema.outputs;
            if (outputs && index < outputs.length) {
                var output = outputs[index];
                if (output) {
                    var name = output.name;
                    if (name) {
                        return name;
                    }
                } 
            }
        }
        if (index == 0) {
            return 'output';
        }
        return '(' + index.toString() + ')';
    }

    getAttributeVisible(operator, name) {
        var schema = this._map[operator];
        if (schema && schema.attributes && schema.attributes.length > 0) {
            if (!schema.attributesMap) {
                schema.attributesMap = {};
                schema.attributes.forEach((attribute) => {
                    schema.attributesMap[attribute.name] = attribute;
                });
            }
            var attribute = schema.attributesMap[name];
            if (attribute && attribute.hasOwnProperty('visible')) {
                return attribute.visible;
            }
        }
        return true;
    }

    getOperatorDocumentation(operator) {
        var schema = this._map[operator];
        if (schema) {
            schema = JSON.parse(JSON.stringify(schema));
            schema.name = operator;
            if (schema.description) {
                schema.description = marked(schema.description);
            }
            if (schema.attributes) {
                schema.attributes.forEach((attribute) => {
                    if (attribute.description) {
                        attribute.description = marked(attribute.description);
                    }
                });
            }
            if (schema.inputs) {
                schema.inputs.forEach((input) => {
                    if (input.description) {
                        input.description = marked(input.description);
                    }
                });
            }
            if (schema.outputs) {
                schema.outputs.forEach((output) => {
                    if (output.description) {
                        output.description = marked(output.description);
                    }
                });
            }
            var template = Handlebars.compile(operatorTemplate, 'utf-8');
            return template(schema);
        }
        return '';
    }
}

class CoreMLError extends Error {
    constructor(message) {
        super(message);
        this.name = 'Error loading CoreML model.';
    }
}