StateMap.js

import DataMap from './DataMap.js';
import {arrayCache} from './utils/array.js';
import {greenSplit} from './utils/string.js';
import recycle from 'recycle';

/**
 * This class defines a state object to use for entity states with helper methods. It includes recycle methods to encourage reuse.
 *
 * @memberof platypus
 * @class StateMap
 * @extends platypus.DataMap
 * @return stateMap {platypus.StateMap} Returns the new StateMap object.
 */
const
    clean = function (arr) {
        const
            cleaned = arrayCache.setUp();

        if (typeof arr[0] === 'string') {
            for (let i = 0; i < arr.length; i++) {
                cleaned.push((i % 2) ? !!arr[i] : arr[i]);
            }
        } else if (typeof arr[0] === 'object') {
            const
                obj = arr[0];

            if (obj.keys && obj.get) {
                const
                    {keys} = obj;
                let i = keys.length;

                while (i--) {
                    cleaned.push(keys[i], !!obj.get(keys[i]));
                }
            } else {
                const
                    keys = Object.keys(obj);
                let i = keys.length;

                while (i--) {
                    cleaned.push(keys[i], !!obj[keys[i]]);
                }
            }
        }

        return cleaned;
    },
    StateMap = function (first) {
        const
            l = arguments.length;
        
        if (l) {
            if ((l === 1) && (typeof first === 'string')) {
                DataMap.call(this);
                this.updateFromString(first);
            } else {
                const
                    cleanedArgs = clean(arguments);

                DataMap.apply(this, cleanedArgs);
                arrayCache.recycle(cleanedArgs);
            }
        } else {
            DataMap.call(this);
        }
    },
    parent = DataMap.prototype,
    proto = StateMap.prototype = Object.create(parent);

Object.defineProperty(proto, 'constructor', {
    configurable: true,
    writable: true,
    value: StateMap
});
    
/**
 * Returns a JSON object of state keys and boolean values.
 *
 * @method platypus.StateMap#toJSON
 * @return {Object} State keys mapped to `true` or `false`.
 */

/**
 * Returns a comma-delimited list of state keys compatible with `updateFromString`. False values are prefixed with `"!"`.
 *
 * @method platypus.StateMap#toString
 * @return {String} The serialized state string.
 */
Object.defineProperty(proto, 'toString', {
    value: function () {
        const
            {keys} = this;
        let i = keys.length,
            parts = arrayCache.setUp();

        while (i--) {
            const
                key = keys[i];

            parts.push(this.get(key) ? key : `!${key}`);
        }

        const
            string = parts.join(',');

        arrayCache.recycle(parts);

        return string;
    }
});

/**
 * Sets the state using the provided string value which is a comma-delimited list such that `"blue,red,!green"` sets the following state values:
 *
 *      {
 *          red: true,
 *          blue: true,
 *          green: false
 *      }
 *
 * @method platypus.StateMap#updateFromString
 * @param states {String} A comma-delimited list of true/false state values.
 * @chainable
 */
Object.defineProperty(proto, 'updateFromString', {
    value: function (states) {
        const
            arr = greenSplit(states, ',');
        let i = arr.length;
        
        while (i--) {
            const
                str = arr[i];

            if (str) {
                if (str.substr(0, 1) === '!') {
                    this.set(str.substr(1), false);
                } else {
                    this.set(str, true);
                }
            }
        }
        
        arrayCache.recycle(arr);
        
        return this;
    }
});

/**
 * Checks whether the provided state matches this state and updates this state to match.
 *
 * @method platypus.StateMap#update
 * @param state {platypus.StateMap} The state that this state should match.
 * @return {Boolean} Whether this state already matches the provided state.
 */
Object.defineProperty(proto, 'update', {
    value: function (newState) {
        const
            {keys} = newState;
        let i = keys.length,
            changed = false;
        
        while (i--) {
            const
                state = keys[i],
                value = newState.get(state);

            if (this.get(state) !== value) {
                this.set(state, value);
                changed = true;
            }
        }
        
        return changed;
    }
});

/**
 * Checks whether the provided state matches all equivalent keys on this state.
 *
 * @method platypus.StateMap#includes
 * @param state {platypus.StateMap} The state that this state should match.
 * @return {Boolean} Whether this state matches the provided state.
 */
Object.defineProperty(proto, 'includes', {
    value: function (otherState) {
        const
            {keys} = otherState;
        let i = keys.length;
        
        while (i--) {
            const
                state = keys[i];

            if (this.get(state) !== otherState.get(state)) {
                return false;
            }
        }
        
        return true;
    }
});

/**
 * Checks whether the provided state matches any equivalent keys on this state.
 *
 * @method platypus.StateMap#intersects
 * @param state {platypus.StateMap} The state that this state should intersect.
 * @return {Boolean} Whether this state intersects the provided state.
 */
Object.defineProperty(proto, 'intersects', {
    value: function (otherState) {
        const
            {keys} = otherState;
        let i = keys.length;
        
        while (i--) {
            const
                state = keys[i];

            if (this.get(state) === otherState.get(state)) {
                return true;
            }
        }
        
        return false;
    }
});

/**
 * Returns StateMap from cache or creates a new one if none are available.
 *
 * @method platypus.StateMap.setUp
 * @return {platypus.StateMap} The instantiated StateMap.
 */
/**
 * Returns StateMap back to the cache. Prefer the StateMap's recycle method since it recycles property objects as well.
 *
 * @method platypus.StateMap.recycle
 * @param {platypus.StateMap} stateMap The StateMap to be recycled.
 */
/**
 * Relinquishes StateMap properties and recycles it.
 *
 * @method platypus.StateMap#recycle
 */
recycle.add(StateMap, 'StateMap', StateMap, function () {
    this.clear();
}, true);

export default StateMap;