import { capitalize } from './util' import { VirtualNode } from './virtual_node' import { EventWrapper } from './event_wrapper'

const nodeWrappers = [];

export class NodeWrapper {

static get selector(){ return `.${this.name}` }

static register(){
    const klass = this

    nodeWrappers.unshift(klass)

    Object.defineProperty(NodeWrapper.prototype, klass.name, {
        get: function(){
            let current = this.parent
            while(current){
                if(current instanceof klass){
                    return current
                }
                current = current.parent
            }
        } 
    })

    Object.defineProperty(NodeWrapper.prototype, `is${capitalize(klass.name)}`, {
        get: function(){
            return this instanceof klass
        } 
    })
}

static instanceFor(node){
    if(!node.$p){
        node.$p = new NodeWrapper(node)
        nodeWrappers.some((klass) => {
            if(node.$p.is(klass.selector)){
                node.$p = new klass(node)
                return true
            }
        })
    }
    return node.$p
}

constructor(node){
    this.node = node
    this.$registeredEventListeners = []
}

get type(){
    return this.node instanceof DocumentType ? '#doctype' : this.node.nodeName.toLowerCase()
}

get attributes(){
    const out = {}
    if(this.node.attributes){
        for(let i = 0; i < this.node.attributes.length; i++){
            out[this.node.attributes[i].name] = this.node.attributes[i].value
        }
    }
    return out
}

get text(){
    return this.node.textContent
}

get realParent(){
    return this.node.parentNode ? this.constructor.instanceFor(this.node.parentNode) : null
}

get parent(){
    if(this.$parent){
        return this.$parent
    }
    return this.realParent
}

get parents(){
    const out = []
    let current = this
    while(current.parent){
        current = current.parent
        out.push(parent)
    }
    return out
}

get children(){
    return [...this.node.childNodes].map(
        node => this.constructor.instanceFor(node)
    )
}

get siblings(){
    if(this.parent){
        return this.parent.children
    } else {
        return [this]
    }
}

get previousSibling(){
    if(this.node.previousSibling){
        return this.constructor.instanceFor(this.node.previousSibling)
    } else {
        return null
    }
}

get nextSibling(){
    if(this.node.nextSibling){
        return this.constructor.instanceFor(this.node.nextSibling)
    } else {
        return null
    }
}

get nextSiblings(){
    const out = []
    let current = this
    while(current.nextSibling){
        current = current.nextSibling
        out.push(current)
    }
    return out
}

get previousSiblings(){
    const out = []
    let current = this
    while(current.previousSibling){
        current = current.previousSibling
        out.push(current)
    }
    return out
}

get descendants(){
    return this.find(() => true)
}

find(selector, out = []){
    this.children.forEach((child) => {
        if(child.is(selector)){
            out.push(child)
        }
        child.find(selector, out)
    })
    return out;
}

is(selector){
    if(typeof selector == 'function'){
        return selector.call(this, this)
    }
    return (this.node.matches || this.node.matchesSelector || this.node.msMatchesSelector || this.node.mozMatchesSelector || this.node.webkitMatchesSelector || this.node.oMatchesSelector || (() => false)).call(this.node, selector)
}

on(name, ...args){
    const fn = args.pop()
    const selector = args.pop()

    const wrapperFn = (event, ...args) => {
        const eventWrapper = EventWrapper.instanceFor(event)
        if(selector){
            if(eventWrapper.target.is(selector)){
                return fn.call(eventWrapper.target, eventWrapper, ...args)
            }
        } else {
            return fn.call(this, eventWrapper, ...args)
        }
    }

    this.node.addEventListener(name, wrapperFn)

    this.$registeredEventListeners.push([name, wrapperFn])

    return this
}

trigger(name, data){
    if (window.CustomEvent && typeof window.CustomEvent === 'function') {
        var event = new CustomEvent(name, { bubbles: true, cancelable: true, detail: data } );
    } else {
        var event = document.createEvent('CustomEvent')
        event.initCustomEvent(name, true, true, data)
    }

    this.node.dispatchEvent(event)

    return this
}

remove(){
    if(this.type != '#doctype'){
        this.realParent.node.removeChild(this.node)
    }
    return this
}

addClass(name){
    this.node.classList.add(name)
    return this
}

removeClass(name){
    this.node.classList.remove(name)
    return this
}

patch(html){
    cleanChildren.call(this)
    patchChildren.call(this, VirtualNode.fromString(html).children)
    initChildren.call(this)
    return this.children
}

append(html){
    return prepend.call(this, html)
}

prepend(html){
    return prepend.call(this, html, this.children[0])
}

insertBefore(html){
    return prepend.call(this.realParent, html, this)
}

insertAfter(html){
    return prepend.call(this.realParent, html, this.nextSibling)
}

}

function cleanChildren(){

this.children.forEach(child => clean.call(child))

}

function clean(){

[...this.node.childNodes].forEach(node => node.$p && clean.call(node.$p))

while(this.$registeredEventListeners.length){
    this.node.removeEventListener(...this.$registeredEventListeners.pop())
}
this.node.$p = undefined

}

function initChildren(){

this.children.forEach(child => initChildren.call(child))

}

function prepend(html, referenceChild){

const out = []
VirtualNode.fromString(html).children.forEach((virtualChild) => {
    out.push(insert.call(this, virtualChild, referenceChild))
})
return out

}

function patch(attributes, virtualChildren){

patchAttributes.call(this, attributes)
patchChildren.call(this, virtualChildren)

}

function patchAttributes(attributes){

if(this.type == '#text' || this.type == '#comment'){
    if(this.node.textContent != attributes.value){
        this.node.textContent = attributes.value
    }
} else if(this.type != '#doctype'){
    const currentAttributes = this.attributes
    Object.keys(currentAttributes).forEach((key) => {
        if(attributes[key] === undefined){
            this.node.removeAttribute(key)
        }
    })
    Object.keys(attributes).forEach((key) => {
        if(currentAttributes[key] != attributes[key]){
            this.node.setAttribute(key, attributes[key])
        }
    })
}

}

function patchChildren(virtualChildren){

const children = [...this.node.childNodes].map(
    node => new NodeWrapper(node)
)

for(let i = 0; i < virtualChildren.length; i++){
    let child = children[0]
    const virtualChild = virtualChildren[i]

    if(child && child.type == virtualChild.type){
        patch.call(children.shift(), virtualChild.attributes, virtualChild.children)
    } else if(virtualChild.type == '#doctype'){
        // ignore
    } else if(virtualChild.type.match(/^#(text|comment)/)){
        insert.call(this, virtualChild, child)
    } else {
        while(children.length > 0 && children[0].type.match(/^#/)){
            children.shift().remove()
        }
        child = children[0]
        if(child && child.type == virtualChild.type){
            patch.call(children.shift(), virtualChild.attributes, virtualChild.children);
        } else {
            insert.call(this, virtualChild, child)
        }
    }
}

while(children.length > 0){
    children.shift().remove()
}

}

function insert(virtualNode, referenceChild, returnNodeWrapper = true){

const { type, attributes, children } = virtualNode

let node

if(type == '#text'){
    node = document.createTextNode(attributes.value)
} else if(type == '#comment'){
    node = document.createComment(attributes.value)
} else {
    node = document.createElement(type)
    Object.keys(attributes).forEach((key) => {
        node.setAttribute(key, attributes[key]) 
    })
}

children.forEach(child => {
    insert.call(new NodeWrapper(node), child, null, false)
})

this.node.insertBefore(
    node,
    referenceChild && referenceChild.node
)

if(returnNodeWrapper){
    return NodeWrapper.instanceFor(node)
}

}