var EventEmitterB = require('EventEmitterB')
var proto = require("proto");
var trimArguments = require("trimArguments")
var observe = require('observe')

var utils = require('./utils')
var domUtils = require('./domUtils')
var blockStyleUtils = require("./blockStyleUtils")

var Style = require("./Style")
Style.isDev = function() {return module.exports.dev}

var components = {};

var setOfBrowserEvents = utils.arrayToMap([
    'abort','afterprint','animationend','animationiteration','animationstart','audioprocess','beforeprint','beforeunload',
    'beginEvent','blocked','blur','cached','canplay','canplaythrough','change','chargingchange','chargingtimechange',
    'checking','click','close','compassneedscalibration','complete','compositionend','compositionstart','compositionupdate','contextmenu',
    'copy','cut','dblclick','decivelight','devicemotion','deviceorientation','deviceproximity','dischargingtimechange','DOMContentLoaded',
    'downloading','drag','dragend','dragenter','dragleave','dragover','dragstart','drop','durationchange','emptied','ended','endEvent',
    'error','focus','focusin','focusout','fullscreenchange','fullscreenerror','gamepadconnected','gamepaddisconnected','hashchange',
    'input','invalid','keydown','keypress','keyup','languagechange','levelchange','load','loadeddata','loadedmetadata','loadend',
    'loadstart','message','mousedown','mouseenter','mouseleave','mousemove','mouseout','mouseover','mouseup','noupdate','obsolete',
    'offline','online','open','orientationchange','pagehide','pageshow','paste','pause','pointerlockchange','pointerlockerror','play',
    'playing','popstate','progress','ratechange','readystatechange','repeatEvent','reset','resize','scroll','seeked','seeking','select',
    'show','stalled','storage','submit','success','suspend','SVGAbort','SVGError','SVGLoad','SVGResize','SVGScroll','SVGUnload','SVGZoom',
    'timeout','timeupdate','touchcancel','touchend','touchenter','touchleave','touchmove','touchstart','transitionend','unload',
    'updateready','upgradeneeded','userproximity','versionchange','visibilitychange','volumechange','waiting','wheel'
])

// events:
    // newParent - emits this when a component gets a new parent
    // parentRemoved - emits this when a component is detached from its parent
var Block = module.exports = proto(EventEmitterB,function(superclass) {

    // static properties

    // constructor
	this.init = function() {
        var that = this

        if(this.name === undefined) {
            throw new Error("The 'name' property is required for Blocks")
        }

        superclass.init.call(this)

        this.attached = false
        this.children = []
        this.state = observe({})
        this.parent = undefined;
        this._styleSetupInfo = []
        this._nativePseudoclassMap = {}

		if (this.id !== undefined) {
			components[this.id] = this;
		}

        if(this.domNode === undefined) {
            this.domNode = domUtils.div()
        }

        this.build.apply(this, arguments)

        //if(module.exports.dev) {
            this.attr('block', this.name)
        //}


        this.domNode.className += ' '+Style.defaultClassName // add the default class
        /*if(this._style === undefined) { // if a style wasn't set by this.build
            this.style = undefined // initialize style to its block or inherited default
        }*/

        // set up dom event handlers
        var ifonHandlers={}
        that.ifon(function(event) {
            if(event in setOfBrowserEvents && (that.excludeDomEvents === undefined || !(event in that.excludeDomEvents))) {
                that.domNode.addEventListener(event, ifonHandlers[event]=function() {
                    that.emit.apply(that, [event].concat(Array.prototype.slice.call(arguments)))
                })
            }
        })
        that.ifoff(function(event) {
            if(event in setOfBrowserEvents && (that.excludeDomEvents === undefined || !(event in that.excludeDomEvents))) {
                that.domNode.removeEventListener(event,ifonHandlers[event])
            }
        })
	}

    // sub-constructor - called by the constructor
    // parameters:
        // label - (Optional) A label that can be used to style a component differently.
                   // Intended to be some string describing what the component is being used for.
                   // Note, tho, that labels are not dynamic - changing the label won't affect styling until a new style is applied to the component)
        // domNode - (Optional) A domNode to be used as the container domNode instead of the default (a div)
    this.build = function(/*[label,] domNode*/) {
        if(arguments.length === 1) {
            this.domNode = arguments[0]
        } else if(arguments.length >= 2) {
            this.label = arguments[0]
            this.domNode = arguments[1]
        }
    }


	// instance properties


	this.domNode;
    this.label;        // a static label that can be used for styling
    this.excludeDomEvents;
    this.children;     // a list of child components that are a part of a Block object (these are used so Styles can be propogated down to child components)


    Object.defineProperty(this, 'label', {
        get: function() {
            return this._label
        }, set: function(v) {
            if(this._label === undefined) {
                this._label = v

                //if(module.exports.dev) {
                    this.attr('label', this._label)
                //}
            } else {
                throw new Error("A Block's label can only be set once (was already set to: "+this._label+")")
            }
        }
    })

    // adds elements to the components main domNode
    // arguments can be one of the following:
        // component, component, component, ...
        // listOfBlocks
    this.add = function() {
        this.addAt.apply(this, [this.domNode.children.length].concat(trimArguments(arguments)))
	}

    // adds nodes at a particular index
    // nodes can be one of the following:
        // component, component, component, ...
        // listOfBlocks
    // todo: look into using document fragments to speed this up when multiple nodes are being added
    this.addAt = function(index/*, nodes...*/) {
        var nodes = normalizeAddAtArguments.apply(this, arguments)

        for (var i=0;i<nodes.length;i++) {
			var node = nodes[i];

            // remove the node from its current parent if necessary
            if(node.parent !== undefined) {
                throw new Error('Node at index '+i+' already has a parent. Remove the node from its parent before adding it somewhere else.')
            }

            this.children.splice(index+i, 0, node)

            if(!isBlock(node)) {
                throw new Error("node is not a Block")
            }

            var beforeChild = this.children[1+i+index]
            if(beforeChild === undefined) {
                this.domNode.appendChild(node.domNode)
            } else {
                this.domNode.insertBefore(node.domNode, beforeChild.domNode)
            }

            node.parent = this;
            node.emit('newParent')
		}

        if(this.attached) {
            for (var i=0;i<nodes.length;i++) {
                var node = nodes[i]
                node.attached = true
                node.style = node._style // rerender its style
            }
        }
    }

	// add a list of nodes before a particular node
    // if beforeChild is undefined, this will append the given nodes
    // arguments can be one of the following:
        // component, component, component, ...
        // listOfBlocks
    this.addBefore = this.addBeforeNode = function(beforeChild) {
        var nodes = trimArguments(arguments).slice(1)
        if(beforeChild === undefined) {
            this.add.apply(this, nodes)
        } else {
            var index = this.children.indexOf(beforeChild)
            this.addAt.apply(this, [index].concat(nodes))
        }
    }


    // arguments can be one of the following:
        // component, component, component, ...
        // index, index, index, ... - each index is the numerical index to remove
        // arrayOfComponents
        // arrayOfIndexes
    this.remove = function() {
        var removals = normalizeRemoveArguments.apply(this, arguments)
        removals = removals.sort(function(a,b) {
            return b-a // reverse sort (so that removing multiple indexes doesn't mess up)
        })

        for(var n=0; n<removals.length; n++) {
            var r = removals[n]
            var c = this.children[r]

            if(c === undefined) {
                throw new Error("There is no child at index "+r)
            }

            c.parent = undefined
            this.children.splice(r, 1)
            this.domNode.removeChild(this.domNode.childNodes[r])

            c.emit('parentRemoved')
        }
    }

    // sets or gets an attribute on the components domNode
    // parameter sets:
    // if one argument is passed, the attribute's value is returned (if there is no attribute, undefined is returned)
    // if there are two arguments passed, the attribute is set
        // if 'value' is undefined, the attribute is removed
    this.attr = function(/*attribute, value OR attributeObject*/) {
        if(arguments.length === 1) {
            if(arguments[0] instanceof Object) {
                var attributes = arguments[0]
                for(var attribute in attributes) {
                    domUtils.setAttribute(this.domNode, attribute, arguments[0][attribute])
                }
            } else {
                var attribute = this.domNode.getAttribute(arguments[0])
                if(attribute === null) {
                    return undefined // screw null
                } else {
                    return attribute
                }
            }
        } else {
            var attribute = arguments[0]
            if(arguments[1] !== undefined) {
                var value = arguments[1]
                domUtils.setAttribute(this.domNode, arguments[0], value)
            } else {
                this.domNode.removeAttribute(attribute)
            }
        }
    }

    Object.defineProperty(this, 'visible', {
        // returns true if the element is visible
        get: function() {
            return this.domNode.style.display !== 'none';

        // sets whether or not the element is visible
        }, set: function(setToVisible) {
            if(setToVisible) {
                if (this._displayStyle !== undefined) {
                    this.domNode.style.display = this._displayStyle // set back to its previous inline style
                    this._displayStyle = undefined
                } else {
                    this.domNode.style.display = ''
                }
            } else {
                if(this.domNode.style.display !== '' && this.domNode.style.display !== 'none') { // domNode has inline style
                    this._displayStyle = this.domNode.style.display
                }

                this.domNode.style.display = 'none'
            }
        }
    })


    Object.defineProperty(this, 'focus', {
        // returns true if the element is in focus
        get: function() {
            return document.activeElement === this.domNode

        // sets whether or not the element is in focus (setting it to true gives it focus, setting it to false blurs it)
        }, set: function(setToInFocus) {
            if(setToInFocus) {
                this.domNode.focus()
            } else {
                this.domNode.blur()
            }
        }
    })

    Object.defineProperty(this, 'style', {
        get: function() {
            return this._style

        // sets the style, replacing one if one already exists
        }, set: function(styleObject) {
            // get active style
                // mix the block-default style with ..
                // .. the current style
                // .. style returned by the $state of current style
                // .. $$pseudoclasses of current + $state styles

            this._style = styleObject
            if(this.attached) {
                var newStyle = getStyle(this)  // must be called after setting _style
                var defaultStyle = this.getDefaultStyle()

                var newCurrentStyle = blockStyleUtils.mixStyles(defaultStyle, newStyle)
                blockStyleUtils.setCurrentStyle(this, newCurrentStyle, defaultStyle)
            }
        }
    })

    Object.defineProperty(this, 'selectionRange', {
        // returns the visible character selection range inside the element
        // returns an array like [offsetStart, offsetEnd]
        get: function() {
            return domUtils.getSelectionRange(this.domNode)

        // sets the visible character selection range
        }, set: function(selection) {
            domUtils.setSelectionRange(this.domNode, selection[0], selection[1])
        }
    })

    this.attach = function(domNode) {
        if(domNode !== undefined)
            attach(domNode, this)
        else
            attach(this)
    }
    this.detach = function(domNode) {
        if(domNode !== undefined)
            detach(domNode, this)
        else
            detach(this)
    }


	// private instance variables/functions

    this.computedStyleMap;  // a map of style objects computed from the Styles set on a given component and its parent components
    this._nativePseudoclassMap; // a map of Block names to a set of native pseudoclass styles and their css selector base (eg: {BlockA: {'.style1:required .style2': styleObject}}

	this._style;             // the object's explicit Style object (undefined if it inherits a style)
    this._currentStyle;      // the object's current Style that will only change if its parent's activeStyle changes, or if a style is explicitly reset on the block
    this._activeStyle;       // the active style depending on pseudoclasses, $state, and defaultStyle

    this._displayStyle;      // temporarily stores an inline display style while the element is hidden (for use when 'show' is called)
    this._styleSetupInfo   // place to put states for setup functions (used for css pseudoclass emulation)
    this._stateChangeHandler // the handler being used for $state style changes

    this.attached           // set to true if the block has been attached to the document (or if one of its ancestors has been)


    // returns the default style of the current Block based on the 'defaultStyle' property set on its constructor (this.constructor)
    // if there is more than one default style, they are merged in order
    // if there is no default style, undefined is returned
    this.getDefaultStyle = function() {
        return blockStyleUtils.getDefaultStyle(this)
    }

    // gets the high-level style of the block, either from the block's explicit style, or inherits from its parent's style map
    function getStyle(block) {
        if(block._style !== undefined) {            // use the block's explicit style if possible
            if(block._style.inherit) {
                var styleToInerit = blockStyleUtils.getInheritingStyle(block, block.parent)
                if(styleToInerit !== undefined)
                    return styleToInerit.mix(block._style)
            }
            // else
            return block._style

        } else {     // otherwise use the parent's computedStyleMap
            return blockStyleUtils.getInheritingStyle(block, block.parent)
        }
    }
});


module.exports.dev = false // set to true to enable dom element naming (so you can see boundaries of components when inspecting the dom)


// appends components to the passed domNode (default: body)
var attach = module.exports.attach = function(/*[domNode,] component or components*/) {
    if(arguments.length > 1) {
        var domNode = arguments[0]
        var components = arguments[1]
    } else {
        if(document.body === null) throw new Error("Your document does not have a body.")
        var domNode = document.body
        var components = arguments[0]
    }

    if(!(components instanceof Array)) {
        var components = [components]
    }

    for(var n=0; n<components.length; n++) {
        domNode.appendChild(components[n].domNode)


        components[n].attached = true
        components[n].style = components[n]._style
    }


}
// removes components from their parents
var detach = module.exports.detach = function(components) {
    if(!(components instanceof Array)) {
        components = [components]
    }

    for(var n=0; n<components.length; n++) {
        var block = components[n]
        block.domNode.parentNode.removeChild(block.domNode)

        markDetached(block)
    }
}

// creates a body tag (only call this if document.body is null)

module.exports.createBody = function(callback) {
    var dom = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null);
    var body = dom.createElement("body")
    dom.documentElement.appendChild(body)
    setTimeout(function() {  // set timeout is needed because the body tag is only added after javascript goes back to the scheduler
        callback()
    },0)
}




// returns a list of indexes to remove from Block.remove's arguments
/*private*/ var normalizeRemoveArguments = module.exports.normalizeRemoveArguments = function() {
    var that = this

    if(arguments[0] instanceof Array) {
        var removals = arguments[0]
    } else {
        var removals = Array.prototype.slice.call(arguments)
    }

    return removals.map(function(removal, parameterIndex) {
        if(isBlock(removal)) {
            var index = that.children.indexOf(removal)
            if(index === -1) {
                throw new Error("The Block passed at index "+parameterIndex+" is not a child of this Block.")
            }
            return index
        } else {
            return removal
        }

    })
}

// returns a list of nodes to add
/*private*/ var normalizeAddAtArguments = module.exports.normalizeAddAtArguments = function() {
    if(arguments.length === 2) {
        if(arguments[1] instanceof Array) {
            return arguments[1]
        } else {
            return [arguments[1]]
        }
    } else { // > 2
        return trimArguments(arguments).slice(1)
    }
}

function isBlock(c) {
    return c.add !== undefined && c.children instanceof Array && c.domNode !== undefined
}
function isDomNode(node) {
    return node.nodeName !== undefined
}

function markDetached(node) {
    node.attached = false
    for(var n=0; n<node.children.length; n++) {
        markDetached(node.children[n])
    }
}