import Node from './node';
import * as types from './types';

export default class Container extends Node {
    constructor (opts) {
        super(opts);
        if (!this.nodes) {
            this.nodes = [];
        }
    }

    append (selector) {
        selector.parent = this;
        this.nodes.push(selector);
        return this;
    }

    prepend (selector) {
        selector.parent = this;
        this.nodes.unshift(selector);
        return this;
    }

    at (index) {
        return this.nodes[index];
    }

    index (child) {
        if (typeof child === 'number') {
            return child;
        }
        return this.nodes.indexOf(child);
    }

    get first () {
        return this.at(0);
    }

    get last () {
        return this.at(this.length - 1);
    }

    get length () {
        return this.nodes.length;
    }

    removeChild (child) {
        child = this.index(child);
        this.at(child).parent = undefined;
        this.nodes.splice(child, 1);

        let index;
        for ( let id in this.indexes ) {
            index = this.indexes[id];
            if ( index >= child ) {
                this.indexes[id] = index - 1;
            }
        }

        return this;
    }

    removeAll () {
        for (let node of this.nodes) {
            node.parent = undefined;
        }
        this.nodes = [];
        return this;
    }

    empty () {
        return this.removeAll();
    }

    insertAfter (oldNode, newNode) {
        newNode.parent = this;
        let oldIndex = this.index(oldNode);
        this.nodes.splice(oldIndex + 1, 0, newNode);

        newNode.parent = this;

        let index;
        for ( let id in this.indexes ) {
            index = this.indexes[id];
            if ( oldIndex <= index ) {
                this.indexes[id] = index + 1;
            }
        }

        return this;
    }

    insertBefore (oldNode, newNode) {
        newNode.parent = this;
        let oldIndex = this.index(oldNode);
        this.nodes.splice(oldIndex, 0, newNode);

        newNode.parent = this;

        let index;
        for ( let id in this.indexes ) {
            index = this.indexes[id];
            if ( index <= oldIndex ) {
                this.indexes[id] = index + 1;
            }
        }

        return this;
    }

    _findChildAtPosition (line, col) {
        let found = undefined;
        this.each(node => {
            if (node.atPosition) {
                let foundChild = node.atPosition(line, col);
                if (foundChild) {
                    found = foundChild;
                    return false;
                }
            } else if (node.isAtPosition(line, col)) {
                found = node;
                return false;
            }
        });
        return found;
    }

    /**
     * Return the most specific node at the line and column number given.
     * The source location is based on the original parsed location, locations aren't
     * updated as selector nodes are mutated.
     * 
     * Note that this location is relative to the location of the first character
     * of the selector, and not the location of the selector in the overall document
     * when used in conjunction with postcss.
     *
     * If not found, returns undefined.
     * @param {number} line The line number of the node to find. (1-based index)
     * @param {number} col  The column number of the node to find. (1-based index)
     */
    atPosition (line, col) {
        if (this.isAtPosition(line, col)) {
            return this._findChildAtPosition(line, col) || this;
        } else {
            return undefined;
        }
    }

    _inferEndPosition () {
        if (this.last && this.last.source && this.last.source.end) {
            this.source = this.source || {};
            this.source.end = this.source.end || {};
            Object.assign(this.source.end, this.last.source.end);
        }
    }

    each (callback) {
        if (!this.lastEach) {
            this.lastEach = 0;
        }
        if (!this.indexes) {
            this.indexes = {};
        }

        this.lastEach ++;
        let id = this.lastEach;
        this.indexes[id] = 0;

        if (!this.length) {
            return undefined;
        }

        let index, result;
        while (this.indexes[id] < this.length) {
            index = this.indexes[id];
            result = callback(this.at(index), index);
            if (result === false) {
                break;
            }

            this.indexes[id] += 1;
        }

        delete this.indexes[id];

        if (result === false) {
            return false;
        }
    }

    walk (callback) {
        return this.each((node, i) => {
            let result = callback(node, i);

            if (result !== false && node.length) {
                result = node.walk(callback);
            }

            if (result === false) {
                return false;
            }
        });
    }

    walkAttributes (callback) {
        return this.walk((selector) => {
            if (selector.type === types.ATTRIBUTE) {
                return callback.call(this, selector);
            }
        });
    }

    walkClasses (callback) {
        return this.walk((selector) => {
            if (selector.type === types.CLASS) {
                return callback.call(this, selector);
            }
        });
    }

    walkCombinators (callback) {
        return this.walk((selector) => {
            if (selector.type === types.COMBINATOR) {
                return callback.call(this, selector);
            }
        });
    }

    walkComments (callback) {
        return this.walk((selector) => {
            if (selector.type === types.COMMENT) {
                return callback.call(this, selector);
            }
        });
    }

    walkIds (callback) {
        return this.walk((selector) => {
            if (selector.type === types.ID) {
                return callback.call(this, selector);
            }
        });
    }

    walkNesting (callback) {
        return this.walk(selector => {
            if (selector.type === types.NESTING) {
                return callback.call(this, selector);
            }
        });
    }

    walkPseudos (callback) {
        return this.walk((selector) => {
            if (selector.type === types.PSEUDO) {
                return callback.call(this, selector);
            }
        });
    }

    walkTags (callback) {
        return this.walk((selector) => {
            if (selector.type === types.TAG) {
                return callback.call(this, selector);
            }
        });
    }

    walkUniversals (callback) {
        return this.walk((selector) => {
            if (selector.type === types.UNIVERSAL) {
                return callback.call(this, selector);
            }
        });
    }

    split (callback) {
        let current = [];
        return this.reduce((memo, node, index) => {
            let split = callback.call(this, node);
            current.push(node);
            if (split) {
                memo.push(current);
                current = [];
            } else if (index === this.length - 1) {
                memo.push(current);
            }
            return memo;
        }, []);
    }

    map (callback) {
        return this.nodes.map(callback);
    }

    reduce (callback, memo) {
        return this.nodes.reduce(callback, memo);
    }

    every (callback) {
        return this.nodes.every(callback);
    }

    some (callback) {
        return this.nodes.some(callback);
    }

    filter (callback) {
        return this.nodes.filter(callback);
    }

    sort (callback) {
        return this.nodes.sort(callback);
    }

    toString () {
        return this.map(String).join('');
    }
}
