# modelview.js

    'use strict';

    /**
     * @fileoverview Class holding "model views", subsets of atoms in a Model used
     * for selection or to perform operations in block
     * @module
     */

    import _ from 'lodash';

    /** A 'view' representing a subset of atom images of a model, used for selection and further manipulations */
    class ModelView {

        /**
         * @param  {Model}  model   Model to use for the view
         * @param  {int[]}  indices Indices of the atom images to include in the view
         */
        constructor(model, indices) {

            this._model = model;
            this._indices = indices;
            this._images = _.map(indices, function(i) {
                return model._atom_images[i];
            });

            this._bonds = {};
            for (var i = 0; i < this._images.length; ++i) {
                var img = this._images[i];
                var bonds = img.bonds;
                for (var j = 0; j < bonds.length; ++j) {
                    var b = bonds[j];
                    this._bonds[b.key] = b;
                }
            }

            this._bonds = Object.values(this._bonds);
        }

        /**
         * Model used by this view
         * @readonly
         * @type {Model}
         */
        get model() {
            return this._model;
        }

        /**
         * Indices of the atom images in this view
         * @readonly
         * @type {int[]}
         */
        get indices() {
            return Array.from(this._indices);
        }

        /**
         * Atom images in this view
         * @readonly
         * @type {AtomImage[]}
         */
        get atoms() {
            return Array.from(this._images);
        }

        /**
         * Number of atom images in this view
         * @readonly
         * @type {int}
         */
        get length() {
            return this._indices.length;
        }

        // Operations on the selected atoms
        // Visibility

        /**
         * Make all atoms in the view visible. Can be chained
         * @return {ModelView}
         */
        show() {
            this._model._setAtomsProperty(this._images, 'visible', true);
            return this;
        }

        /**
         * Make all atoms in the view invisible. Can be chained
         * @return {ModelView}
         */
        hide() {
            this._model._setAtomsProperty(this._images, 'visible', false);
            return this;
        }

        /**
         * Run a function on each AtomImage, returning an Array of the results.
         *
         * @param  {Function}   func    Function to run, should take AtomImage and
         *                              index as arguments
         *
         * @return {Array}              Return values
         */
        map(func) {
            var returns = [];
            for (var i = 0; i < this.length; ++i) {
                returns.push(func(this._images[i], i));
            }
            return returns;
        }

        /**
         * Perform a further search within the atoms included in this ModelView.
         *
         * @param  {Array}   query    Query for the search, formatted as for
         *                            the Model.find function.
         *
         * @return {ModelView}        Result of the query
         */
        find(query) {
            var found = this._model._qparse.parse(query);
            return new ModelView(this._model,
                _.intersectionWith(this._indices, found));
        }

        // Logical operations with another ModelView
        /**
         * Intersection with another ModelView
         * @param  {ModelView} mview    Other view
         * @return {ModelView}          Result
         */
        and(mview) {
            if (this._model != mview._model)
                throw 'The two ModelViews do not refer to the same Model';
            return new ModelView(this._model,
                _.intersectionWith(this._indices, mview._indices));
        }

        /**
         * Union with another ModelView
         * @param  {ModelView} mview    Other view
         * @return {ModelView}          Result
         */
        or(mview) {
            if (this._model != mview._model)
                throw 'The two ModelViews do not refer to the same Model';
            return new ModelView(this._model,
                _.unionWith(this._indices, mview._indices));
        }

        /**
         * Exclusive OR with another ModelView
         * @param  {ModelView} mview    Other view
         * @return {ModelView}          Result
         */
        xor(mview) {
            if (this._model != mview._model)
                throw 'The two ModelViews do not refer to the same Model';
            return new ModelView(this._model,
                _.xorWith(this._indices, mview._indices));
        }

        /**
         * Complement to this ModelView
         * @return {ModelView}          Result
         */
        not() {
            var indices = _.xorWith(this._indices, _.range(this._model._atom_images.length));
            return new ModelView(this._model, indices);
        }

        /**
         * Internal function used to turn a single value, array of values, or
         * function into an array of values
         * @private
         */
        _standardValueArray(value) {

            if (_.isFunction(value)) {
                value = this.map(value);
            } else if (!(value instanceof Array)) {
                value = Array(this.length).fill(value);
            }

            return Array.from(value);
        }

        /**
         * Set some property of the atoms within the ModelView.
         *
         * @param {String}              name    Name of the property to set
         * @param {int|Array|function}  value   Value to set for the atoms. It can
         *                                      be either:
         *
         *                                      1. a single value for all of them
         *                                      2. an Array of values as long as
         *                                      the ModelView
         *                                      3. a function that accepts an
         *                                      AtomImage and an index and returns
         *                                      a value
         *
         *                                      If left empty, the property is
         *                                      restored to its default value.
         */
        setProperty(name, value=null) {

            if (name[0] == '_') {
                // Assignment of hidden properties not supported!
                throw 'Can not assign a value to hidden properties';
            }

            value = this._standardValueArray(value);

            for (var i = 0; i < this.length; ++i) {
                var v = value[i];
                var aimg = this._images[i];
                aimg[name] = v;
            }

            return this;
        }

        /**
         * Add labels to the atom images in this ModelView
         *
         * @param {String | String[] | Function}    text    Text of the labels,
         *                                                  as single value, array,
         *                                                  or function returning a
         *                                                  string for each atom image.
         * @param {String | String[] | Function}    name    Name of the label
         * @param {Object | Object[] | Function}    args    Arguments for creating the label
         */
        addLabels(text, name = 'label', args = {}) {

            // Defaults
            if (!text) {
                text = function(a) {
                    return a.element;
                }
            }

            text = this._standardValueArray(text);
            name = this._standardValueArray(name);
            args = this._standardValueArray(args);

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                aimg.addLabel(String(text[i]), name[i], args[i]);
            }

            return this;
        }

        /**
         * Remove labels from the atom images in this ModelView
         *
         * @param {String | String[] | Function}    name    Name of the labels to remove
         */
        removeLabels(name = 'label') {

            name = this._standardValueArray(name);

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                aimg.removeLabel(name[i]);
            }

            return this;
        }

        /**
         * Get or set labels' properties for the atom images in this ModelView
         *
         * @param {String | String[] | Function}    name        Name of the labels
         * @param {String | String[] | Function}    property    Property to get or set
         * @param {Any | Any[] | Function}          value       If not provided, get. If provided,
         *                                                      set this value
         */
        labelProperties(name = 'label', property = 'color', value = null) {

            name = this._standardValueArray(name);
            property = this._standardValueArray(property);

            var ans = null;
            if (value !== null) {
                value = this._standardValueArray(value);
                ans = [];
            }

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                if (value !== null) {
                    aimg.labelProperty(name[i], property[i], value[i]);
                } else {
                    ans.push(aimg.labelProperty(name[i], property[i]));
                }
            }

            if (value === null)
                return ans;
            else
                return this;
        }

        /**
         * Add ellipsoids to the atom images in this ModelView
         *
         * @param {Object | Object[] | Function}    data    Data to use for the ellipsoid
         *                                                  (see AtomImage.addEllipsoid for details)
         * @param {String | String[] | Function}    name    Name of the ellipsoids
         * @param {Object | Object[] | Function}    args    Arguments for creating the ellipsoids
         */
        addEllipsoids(data, name = 'ellipsoid', args = {}) {

            data = this._standardValueArray(data);
            name = this._standardValueArray(name);
            args = this._standardValueArray(args);

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                aimg.addEllipsoid(data[i], name[i], args[i]);
            }

            return this;
        }

        /**
         * Remove ellipsoids from the atom images in this ModelView
         *
         * @param {String | String[] | Function}    name    Name of the ellipsoids to remove
         */
        removeEllipsoids(name = 'ellipsoid') {

            name = this._standardValueArray(name);

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                aimg.removeEllipsoid(name[i]);
            }

            return this;
        }

        /**
         * Get or set ellipsoids' properties for the atom images in this ModelView
         *
         * @param {String | String[] | Function}    name        Name of the ellipsoids
         * @param {String | String[] | Function}    property    Property to get or set
         * @param {Any | Any[] | Function}          value       If not provided, get. If provided,
         *                                                      set this value
         */
        ellipsoidProperties(name = 'ellipsoid', property = 'color', value = null) {

            name = this._standardValueArray(name);
            property = this._standardValueArray(property);

            var ans = null;
            if (value !== null) {
                value = this._standardValueArray(value);
                ans = [];
            }

            for (var i = 0; i < this.length; ++i) {
                var aimg = this._images[i];
                if (value !== null) {
                    aimg.ellipsoidProperty(name[i], property[i], value[i]);
                } else {
                    ans.push(aimg.ellipsoidProperty(name[i], property[i]));
                }
            }

            if (value === null)
                return ans;
            else
                return this;
        }

    }

    export {
        ModelView
    }