//
// -- IRIS Toolkit - JSON Interface over TLS
//
//  Copyright (c) 2014 ASPECTRON Inc.
//  All Rights Reserved.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
//

var _ = require("underscore");
var fs = require("fs");
var net = require("net");
var tls = require('tls');
var colors = require('colors');
var crypto = require('crypto');
var events = require('events');
var util = require('util');
var UUID = require('node-uuid');

if(!global.dpc)
    global.dpc = function(t,fn) { if(typeof(t) == 'function') setTimeout(t,0); else setTimeout(fn,t); }

var RPC_VERSION = 1;

var config = {
    verbose : 1,    // 0 = suppress; 1 = basic; 2 = full
    cipher : true,
    debug : false
}

function encrypt(text, cipher, key){
    var cipher = crypto.createCipher(cipher, key)
    var crypted = cipher.update(text, 'utf8', 'hex')
    crypted += cipher.final('hex');
    return crypted;
}

function decrypt(text, cipher, key){
    var decipher = crypto.createDecipher(cipher, key)
    var dec = decipher.update(text,'hex', 'utf8')
    dec += decipher.final('utf8');
    return dec;
}

function Stream(tlsStream, iface, address) {
	var self = this;
	self.tlsStream = tlsStream;
    self.iface = iface;
	self.buffer = '';
    self.address = tlsStream._host || tlsStream.servername || address || 'N/A';
    self.serverName = tlsStream.servername;
    self.pending = { }

    if(iface.rejectUnauthorized && !tlsStream.authorized)
        return tlsStream.end();

    iface.connectionCount++;

    tlsStream.setEncoding('utf8');

    tlsStream.on('data', function (data) {
        /*if (self.buffer.length + data.length > 1024 * 65) {
            self.buffer = data;
        }*/

        self.buffer += data;

        var idx = self.buffer.indexOf('\n');
        while(~idx) {
            var msg = self.buffer.substring(0, idx);
            self.buffer = self.buffer.substring(idx + 1);
            try {
                if(self.cipher)
                    msg = decrypt(msg, self.cipher, iface.pk);

                iface.emit('stream::message',JSON.parse(msg), self);
            }
            catch (ex) {
                console.log(ex.stack);
                tlsStream.end();
                break;
            }

            idx = self.buffer.indexOf('\n');
        }

    });

    tlsStream.on('error', function (err) {
    	iface.connectionCount--;
//        if(config.verbose)
        if(config.verbose > 1 || config.verbose && err.code != 'ECONNREFUSED')
            console.log("iris-rpc tls stream error:", err.toString()/*.message*/, ' | ' ,self.iface.designation+'@'+self.address);
    	iface.emit('stream::error', err, self);
    });

    tlsStream.on('end', function () {
    	iface.connectionCount--;
        if(config.verbose > 1)
            console.log("iris-rpc tls stream end");
        iface.emit('stream::end', self)
    });

    self.end = function() {
    	iface.connectionCount--;
    	tlsStream.end();
    }

    self.writeJSON = function(msg) {
//        if(config.debug)
//            console.log('<--'.bold,msg);
        self.tlsStream.write(JSON.stringify(msg) + '\n');
        return true;
    }

    self.writeTEXT = function(text, callback) {
        // console.log(text);
        //if(config.debug)
            //console.log('<--',text);
        self.tlsStream.write(text + '\n', callback);
        return true;
    }
}

function Interface(options) {
	var self = this;
    events.EventEmitter.call(this);

    if(!options.certificates)
        throw new Error("iris-rpc options - missing 'certificates' argument");
    if(!options.auth && !options.secret)
        throw new Error("iris-rpc options - missing 'auth' argument");
    if(!options.uuid)
        throw new Error("iris-rpc options - missing 'uuid' argument");
//    if(!options.mac)
//        throw new Error("iris-rpc options - mac is required");
// console.log("UUID".bold,options.uuid);

    // console.log("Creating RPC instance:".green.bold, options);

    self.uuid = options.uuid;
    self.designation = options.designation;
    self.mac = options.mac;
    self.filters = [ ]
    self.listeners_ = [ self ]
    self.streams = { }
    // self.pending = { }
    self.timeout = options.timeout || 60 * 1000;
    self.pingFreq = options.pingFreq || 3 * 1000;
    self.pingDataObject = options.pingDataObject;
    self.pk = crypto.createHash('sha512').update(options.auth || options.secret).digest('hex');
    self.rejectUnauthorized = options.rejectUnauthorized || false;
    self.signatures = options.signatures || true;
    self.cipher = options.cipher || config.cipher;
    if(self.cipher === true)
        self.cipher = 'aes-256-cbc';
    self.routes = {
        local : options.routes || null,
        remote : { }
    }

    self.iface = { }

    // Server
    self.iface['rpc::auth::request'] = function(msg, stream) {
        var vector = crypto.createHash('sha512').update(crypto.randomBytes(512)).digest('hex');
        stream.vector = vector;
        stream.writeJSON({
            op : 'rpc::auth::challenge',
            v : RPC_VERSION,
            vector : vector
        });
    }

    // Client
    self.iface['rpc::auth::challenge'] = function(msg, stream) {

        var vector = msg.vector;
        if(!vector) {
            console.log("iris-rpc: no vector in auth message");
            stream.end();
            return;
        }

        var auth = crypto.createHmac('sha256', self.pk).update(vector).digest('hex');

        var msg = {
            op : 'rpc::auth::response',
            v : RPC_VERSION,
            cipher : self.cipher ? encrypt(self.cipher, 'aes-256-cbc', self.pk) : false,
        }

        var data = {
            // op : 'auth',
            auth : auth,
            signatures : self.signatures,
            uuid : self.uuid,
            mac : self.mac,
//            node : options.node || UUID.v1(),
            designation : self.designation || ''
        }

        // console.log("RPC CLIENT SENDING INIT DATA:".blue.bold, data);

        msg.data = self.cipher ? encrypt(JSON.stringify(data), 'aes-256-cbc', self.pk) : data;

        stream.writeJSON(msg);

        stream.signatures = self.signatures;
        var seq_auth = crypto.createHmac('sha1', self.pk).update(vector).digest('hex');
        stream.sequenceTX = parseInt(seq_auth.substring(0, 8), 16);
        stream.sequenceRX = parseInt(seq_auth.substring(8, 16), 16);

        if(self.cipher)
        	stream.cipher = self.cipher;

//        self.streams[stream.uuid] = stream;
    }

    // Server
    self.iface['rpc::auth::response'] = function(msg, stream) {
        try {

            var data = msg.data;
            if(!data) {
                console.log("iris-rpc auth packet missing data (peer at "+stream.address+"):", msg);
                stream.end();
                return;
            }

            if(msg.cipher) {
                stream.cipher = decrypt(msg.cipher, 'aes-256-cbc', self.pk);
                data = JSON.parse(decrypt(data, stream.cipher, self.pk));
            }

            // console.log("RPC SERVER RECEIVING INIT DATA:".blue.bold, data);

            if(!data.uuid || !data.auth) {
                console.log("iris-rpc auth packet missing auth, uuid or designation (peer at "+stream.address+"):", data);
                stream.end();
                return;
            }

            var auth = crypto.createHmac('sha256', self.pk).update(stream.vector).digest('hex');
            if(auth != data.auth) {
                console.log("iris-rpc auth failure:", data);
                stream.end();
                return;
            }

            stream.uuid = data.uuid;
            stream.mac = data.mac;
            //stream.node = data.node;
            stream.designation = data.designation;
            //stream.nid = data.designation ? data.node+'-'+data.designation : data.node;
            stream.signatures = self.signatures || data.signatures;

            var sig_auth = crypto.createHmac('sha1', self.pk).update(stream.vector).digest('hex');
            stream.sequenceRX = parseInt(sig_auth.substring(0, 8), 16);
            stream.sequenceTX = parseInt(sig_auth.substring(8, 16), 16);

        }
        catch(ex) {
            console.log("generic failure during auth:", ex.toString());
            stream.end();
            return;
        }

        // console.log(("SERVER CONNECTING STREAM "+stream.uuid).green.bold);
        self.streams[stream.uuid] = stream;

        self.dispatch(stream.uuid, {
        	op : 'rpc::init',
            v : RPC_VERSION,
        	data : {
                uuid : self.uuid,
                mac : self.mac,
        		//node : options.node || UUID.v1(),
        		designation : self.designation || ''
        	},
        	routes : _.keys(self.routes.local)
        })
    }

    // Servre & Client
    self.iface['rpc::init'] = function(msg, stream) {

		var data = msg.data;
		if(data) {    // Client

            stream.uuid = data.uuid;
            stream.mac = data.mac;
			//stream.node = data.node;
			stream.designation = data.designation;
            //stream.uuid = data.designation ? data.node+'-'+data.designation : data.node;

            // console.log(("CLIENT CONNECTING STREAM "+stream.uuid).green.bold);
            self.streams[stream.uuid] = stream;

            self.dispatch(stream.uuid, {
                op : 'rpc::init',
                v : RPC_VERSION,
                routes : _.keys(self.routes.local)
            })
		}

		_.each(msg.routes, function(uuid) {
			self.routes.remote[uuid] = stream;
		})

		stream.connected = true;
        self.emitToListeners('connect', stream.address, stream.uuid, stream);
    }

    self.iface['rpc::online'] = function(msg, stream) {
        //var nid = msg.nid;
        self.routes.remote[msg.uuid] = stream;
    }

    self.iface['rpc::offline'] = function(msg, stream) {
        delete self.routes.remote[uuid];
    }

	self.on('stream::message', function(msg, stream) {

        // if(config.debug)
            // console.log('-->', msg);

        if(msg._sig) {
            if(config.debug)
                console.log("sequenceRX:".cyan.bold,stream.sequenceRX);
            var sig = crypto.createHmac('sha256', self.pk).update(stream.sequenceRX+'').digest('hex').substring(0, 16);
            if(msg._sig != sig) {
                console.log("should be ",sig,"is",msg._sig);
                console.log("iris-rpc signature failure:", msg);
                stream.end();
                return;
            }
            stream.sequenceRX++;
            delete msg._sig;
        }

        if(self.iface[msg.op]) {
        	self.iface[msg.op].call(self, msg, stream);
        	return;
        }
        else
        if(!stream.uuid) {
            console.log("iris-rpc foreign connection "+stream.address+", closing");
            stream.end();
            return;
        }

        if(!self.filters.length)
            return digestMessage(msg, stream);

        var filters = self.filters.slice();

        _digest();

        function _digest() {
            var filter = filters.shift();
            if(!filter)
                return digestMessage(msg, stream);

            filter.call(self, msg._req ? msg.req : msg, stream, function(err, disallow) {
                if(err)
                    return _fail(err);

                if(disallow)
                    return _fail(disallow);

                _digest();
            })
        }

        function _fail(err) {
            if(msg._req) {
                self.dispatchToStream(stream, {
                    _resp : msg._req,
                    err : err
                });
            }
        }
    })

    function digestMessage(msg, stream) {
        try
        {

            if(msg._req) {
                var listeners = self.listeners(msg.req.op);
                if(listeners.length == 1) {
                    //var emitted = self.emit(
                    try {
                        listeners[0].call(self, msg.req, function(_err, resp) {
                            var err = (_err instanceof Error) ? {
                                    _Error : true,
                                    name : _err.name,
                                    message : _err.message,
                                    stack : _err.stack,
                                } : _err;

                            self.dispatchToStream(stream, {
                                _resp : msg._req,
                                err : err,
                                resp : resp,
                            });
                        })
                    } catch(ex) {
                        console.log("responding with error".red.bold,ex);
                        self.dispatchToStream(stream, {
                            _resp : msg._req,
                            err : ex
                        });
                    }
                }
                else
                if(listeners.length)
                {
                    self.dispatchToStream(stream, {
                        _resp : msg._req,
                        err : { error : "Too many handlers for '"+msg.req.op+"'" }
                    });
                }
                else
                {
                    self.dispatchToStream(stream, {
                        _resp : msg._req,
                        err : { error : "No such handler '"+msg.req.op+"'" }
                    });
                }
            }
            else
            if(msg._resp) {
                var pending = stream.pending[msg._resp];
                if(pending) {
                    try {

                        var err = msg.err;
                        if(err && err._Error) {
                            err = global[msg.err.name] ? new global[msg.err.name](msg.err.message) : new Error(msg.err.message);
                            err.stack = msg.err.stack;
                        }

                        pending.callback(err, msg.resp);
                    }
                    catch(ex) {
                        console.error("Error in callback for response:",msg);
                        console.error(ex.stack);
                    }
                    delete stream.pending[msg._resp];
                }
                else {
                    console.error('iris-rpc: no pending callback for response:'.magenta.bold, msg);
                }
            }
            else
            {
                var uuid = msg._r ? msg._r.uuid : stream.uuid;
                self.digestCallback && self.digestCallback(msg, uuid, stream);
                msg.op && self.emitToListeners(msg.op, msg, uuid, stream);
            }

        } catch(ex) {
            console.error("iris-rpc: error while processing message".magenta.bold);
            console.error(ex.stack);
            self.emitToListeners('rpc::error', ex, msg);
        }
	}

	self.on('stream::error', function(err, stream) {

        if(err.code != 'ECONNREFUSED')
            self.emitToListeners('disconnect', stream.uuid, stream);
        pendingCleanup(stream);
        // console.log(("RPC DISCONNECTING STREAM "+stream.uuid).green.bold);
        delete self.streams[stream.uuid];
	})

	self.on('stream::end', function(stream) {
        self.emitToListeners('disconnect', stream.uuid, stream);
        pendingCleanup(stream);
        // console.log(("RPC DISCONNECTING STREAM "+stream.uuid).green.bold);
        delete self.streams[stream.uuid];
	})

    //-----

    self.emitToListeners = function() {
        var args = arguments;
        _.each(self.listeners_, function(listener) {
            try {
                listener.emit.apply(listener, args);
            } catch(ex) {
                console.error("iris-rpc: error while processing message".magenta.bold);
                console.error(ex.stack);
                self.emitToListeners('rpc::error', ex);
            }
        })
    }

	self.dispatchToStream = function(stream, _msg, callback) {
        var msg = null;

        if(callback) {
            var req_uuid = UUID.v1();
            // msg._req = req_uuid;
            stream.pending[req_uuid] = {
                uuid : req_uuid,
                req : _msg,
                callback : callback,
                ts : Date.now()
            }

            msg = {
                _req : req_uuid,
                req : _msg
            }
        }
        else
            msg = _.clone(_msg);

        if(self.routing)
            msg._uuid = stream.uuid;

        if(stream.signatures) {
            if(config.debug)
                console.log("sequenceTX:".cyan.bold,stream.sequenceTX);
            msg._sig = crypto.createHmac('sha256', self.pk).update(stream.sequenceTX+'').digest('hex').substring(0, 16);
            stream.sequenceTX++;
        }

//        if(config.debug)
//            console.log('<--'.bold, msg);

        var text = JSON.stringify(msg);
        if(stream.cipher)
            text = encrypt(text, stream.cipher, self.pk);
        // if(msg.op != 'ping') console.log("sending text...",msg);
        stream.writeTEXT(text);

        return true;
	}

    self.dispatch = function (uuid, msg, callback) {
    	if(_.isObject(uuid)) {
    		callback = msg;
            msg = uuid;
    		uuid = null;

            if(callback && !_.size(self.streams)) {
                return callback({ error : "RPC stream is not connected", disconnected : true, req : msg });
            }
            else
            if(callback && _.size(self.streams) > 1) {
                return callback({ error : "Multiple streams connected (not supported by RPC)", req : msg });
            }

	    	_.each(self.streams, function(stream, uuid) {
                // console.log(("dispatching to stream "+uuid).cyan.bold+("  op: "+msg.op).bold+" (multi)");
	    		self.dispatchToStream(stream, msg, callback);
	    	})
    	}
    	else {
            // console.log(("dispatching to stream "+uuid).cyan.bold+("  op: "+msg.op).bold+" (single)");
    		var stream = self.streams[uuid];
    		if(!stream) {
	            console.error('iris-rpc: no such stream present:'.magenta.bold, uuid);
	            callback && callback(new Error("iris-rpc: no such stream present"))
	            return;
    		}

            if(!msg) {
                console.error('iris-rpc: dispatch() got empty message'.magenta.bold);
                callback && callback(new Error("iris-rpc: no such stream present"))
            }
            else
            	self.dispatchToStream(stream, msg, callback);
    	}
    }

    self.response = function(msg, resp) {
        var rid = msg._rid;
    }

    self.filter = function(callback) {
        self.filters.push(callback);
    }

    self.digest = function(callback) {
        self.digestCallback = callback;
    }

    self.registerListener = function(listener) {
    	self.listeners_.push(listener);
    }

    self.setPingDataObject = function(o) {
        self.pingDataObject = o;
    }

    self.ping = function() {
        self.dispatch({ op : 'ping', data : self.pingDataObject});
    }

    function ping() {
        self.ping();
        dpc(self.pingFreq, ping);
    }

    if(options.ping || options.pingFreq || options.pingDataObject) {
        dpc(function () {
            ping();
        })
    }

    function pendingCleanup(stream) {
        _.each(stream.pending, function(pending) {
            pending.callback.call(self, { error : "Connection Terminated", disconnected : true, req : pending.req } );
        })
    }

    function timeoutMonitor() {
        var ts = Date.now();
        var purge = [ ]
        _.each(self.streams, function(stream) {
            _.each(stream.pending, function(pending, uuid) {
                var timeout = pending.req.op_timeout || self.timeout;
                if(ts - pending.ts > timeout) {
                    pending.callback.call(self, { error : "Connection Timed Out", timeout : self.timeout, req : pending.req } );
                    purge.push({ stream : stream, uuid : uuid });
                }
            })
        })

        _.each(purge, function(o) {
            delete o.stream.pending[o.uuid];
        })

        dpc(1000, timeoutMonitor);
    }

    timeoutMonitor();

}

util.inherits(Interface, events.EventEmitter);

function Client(options) {
	var self = this;
	Interface.call(this, options);

    if(!options.address)
        throw new Error("iris-rpc::Client requires address argument");

	if(_.isArray(options.address)) {
		_.each(options.address, function(address) {
			createConnection(address);
		})
	}
	else
		createConnection(options.address);

	function createConnection(address) {

	    var addr = address.split(':');

	    if(config.verbose > 1)
	        console.log("iris-rpc connecting to address:", address);

	    var tlsOptions = { }
	    tlsOptions.host = addr[0];
	    tlsOptions.port = parseInt(addr[1]);
	    tlsOptions.rejectUnauthorized = options.rejectUnauthorized || false;
	    _.extend(tlsOptions, options.certificates);

        self.auth = false;
        var tlsStream = tls.connect(tlsOptions, function () {
            console.log('iris-rpc '+self.designation+' connected to server @'+address+', SSL certificate is', tlsStream.authorized ? 'authorized' : 'unauthorized');
            if(self.rejectUnauthorized && !tlsStream.authorized)
                return tlsStream.end();

            stream.writeJSON({ op : 'rpc::auth::request'});
        });

        var stream = new Stream(tlsStream, self, addr[0]);
        stream.address = address;
	}

    self.on('stream::error', function(err, stream) {
        dpc(1000, function() {
        	createConnection(stream.address);
        });
    })

    self.on('stream::end', function(stream) {
        dpc(1000, function() {
        	createConnection(stream.address);
        });
    })

    self.isConnected = function() {
 		for(var i in self.streams)
            return true;
        return false;
    }
}

util.inherits(Client, Interface);

function Server(options, initCallback) {
	var self = this;
	Interface.call(this, options);

    if(!options.port)
        throw new Error("iris-rpc::Server requires port argument");

    var port = options.port;
    var host = options.host;
    if(typeof(options.port) == 'string') {
        var parts = options.port.split(':');
        if(parts.length != 2)
            throw new Error("iris-rpc::Server port string must have 'host:port' format, instead it is: "+options.port);
        host = parts[0];
        port = parseInt(parts[1])
    }

    self.tlsServer = tls.createServer(options.certificates, function (tlsStream) {
        if(self.rejectUnauthorized && !stream.authorized)
            return stream.end();
        var stream = new Stream(tlsStream, self);
    });

    var args = [ port ];
    if(host)
        args.push(host);
    args.push(function(err) {
        if(err)
            console.error('iris-rpc server listen error on '+options.port, err);
        self.emitToListeners('server::listen', err);
        initCallback && initCallback(err);
    })

    self.tlsServer.listen.apply(self.tlsServer, args);

    self.on('stream::error', function(err, stream) {
        stream.tlsStream.end();
    })

    // self.on('stream::end', function(stream) {
    // })
}

util.inherits(Server, Interface);

function Multiplexer(options, config, rpcTitle) {
    var self = this;
    self.active = { }
    self.links = createFromConfig(options, config, rpcTitle);
//    if(!_.isArray(self.links))
//        self.links = [ self.links ];
//    console.log("links:",self.links);

    self.on = function(op, callback) {
        _.each(self.links, function(rpc) {
            rpc.on(op, callback);
        })
    }

    self.on('connect', function(address, uuid, stream) {
        self.active[uuid] = stream;
    })

    self.on('disconnect', function(uuid, stream) {
        delete self.active[uuid];
    })

    self.registerListener = function(listener) {
        _.each(self.links, function(rpc) {
            rpc.registerListener(listener);
        })
    }

    self.dispatch = function(uuid, msg, callback) {
        var args = arguments;
        if (_.isObject(uuid)){
             _.each(self.links, function(rpc) {
                rpc.dispatch.apply(rpc, args);
            })
            return
        }

        var rpc = _.find( self.links, function(s){
            return !!s.streams[uuid];
        });
        if (!rpc){
            console.error('iris-rpc: no such stream present:'.magenta.bold, uuid);
            callback && callback(new Error("iris-rpc: no such stream present"))
            return;
        }
        rpc.dispatch.apply(rpc, args);
    }
}

// Router - allows client->router->server (or client->router<-client) connectivity.
// This class is meant to help when a server is unable to handle maximum number
// of connections.

function Router(options) {
    var self = this;

    self.frontend = new Server({
        port : options.port,
        auth : options.auth || options.secret,
        certificates : options.certificates,
        routing : true
    })

    // connect to relay destination
    if(options.client) {
        self.backend = new Client({
            address: options.client.address,
            auth: options.client.auth || options.client.secret || options.auth || options.secret,
            certificates: options.certificates || options.client.certificates,
            //node: options.node,
            designation: 'router',
            routes : self.frontend.streams
        })
    }
    else
    if(options.server) {
        self.backend = new Server({
            port : options.server.port,
            auth : options.server.auth || options.server.secret || options.auth || options.secret,
            certificates : options.certificates || options.server.certificates,
            routes : self.frontend.streams
        })
    }
    else
        throw new Error("iris-rpc::Router() requires client or server")

    self.frontend.on('connect', function(address, uuid, stream) {
        self.backend.dispatch({ op : 'rpc::online', uuid : uuid });
    })

    self.frontend.on('disconnect', function(uuid, stream) {
        self.backend.dispatch({ op : 'rpc::offline', uuid : uuid });
    })

    self.backend.on('connect', function(address, uuid, stream) {
        self.frontend.dispatch({ op : 'rpc::online', uuid : uuid });
    })

    self.backend.on('disconnect', function(uuid, stream) {
        self.frontend.dispatch({ op : 'rpc::offline', uuid : uuid });
    })

    self.frontend.digest(function(msg, uuid, stream) {
        msg._r = {
            uuid : uuid,
            designation : stream.designation,
            uuid : stream.uuid
        }

        self.backend.dispatch(msg);
    })

    self.backend.digest(function(msg, uuid) {
        self.frontend.dispatch(msg._uuid, msg);
    })
}

function createFromConfig(options, config, rpcTitle) {

    function createInstance(options, config, rpcTitle) { // rpcConfig) {
        var rpcConfig = _.extend({ }, options);
        _.extend(rpcConfig, config);
        if(!rpcConfig.port && !rpcConfig.address) {
            console.error("Error:".red.bold,rpcConfig);
            throw new Error("port or address required in rpc config");
        }
        if(rpcConfig.port && rpcConfig.address) {
            console.error("Error:".red.bold,rpcConfig);
            throw new Error("both port and address present in rpc config");
        }
        if(rpcConfig.port) {
            return new Server(rpcConfig, function(err) {
                console.log((rpcTitle.toUpperCase()+" RPC").bold+" server listening on",(rpcConfig.port+'').bold);
            });
        }
        else {
            return new Client(rpcConfig);
        }
    }

    if(config.address || config.port) {
        var ret = { }
        ret[rpcTitle] = createInstance(options, config, rpcTitle || options.designation);
        return ret;
    }
    else {
        var ret = { }
        _.each(config, function(rpcConfig, rpcTitle) {
            ret[rpcTitle] = createInstance(options, rpcConfig, rpcTitle);
        })
        return ret;
    }
}

function Certificates(certificates) {
    var self = this;

    function locateCertificateFile(filename, ignore) {
        var file = filename;
        var parts = file.split('.');
        parts.splice(parts.length-1, 0, '.local');
        var local = parts.join();
        if(fs.existsSync(local))
            return local;
        if(!ignore && !fs.existsSync(file)) {
            console.log("Unable to locate certificate file:".red.bold, file.bold);
            throw new Error("Unable to locate certificate file");
        }
        else
        if(ignore && !fs.existsSync(file))
            return null;

        return file;
    }

    self.initCertificates = function(callback) {
        if(self.verbose)
            console.log('iris-app: loading certificates from ', certificates);
        if(self.certificateData) {
            console.error("Warning! initCertificates() is called twice!");
            callback && callback();
            return;
        }

        var ca_chain;
        if(typeof(certificates) == 'string') {

            self.certificateData = {
                key: fs.readFileSync(locateCertificateFile(certificates+'.key')).toString(),
                cert: fs.readFileSync(locateCertificateFile(certificates+'.crt')).toString(),
                ca: [ ]
            }

            ca_chain = certificates+'.ca';
        }
        else
        {
            self.certificateData = {
                key: fs.readFileSync(locateCertificateFile(certificates.key)).toString(),
                cert: fs.readFileSync(locateCertificateFile(certificates.crt)).toString(),
                ca: [ ]
            }

            ca_chain = certificates.ca;
        }

        if(ca_chain) {

            var ca_chain_file = locateCertificateFile(ca_chain, true);

            if(ca_chain_file) {

                var cert = [ ]
                var chain = fs.readFileSync(ca_chain_file).toString().split('\n');
                _.each(chain, function(line) {
                    cert.push(line);
                    if(line.match('/-END CERTIFICATE-/')) {
                        self.certificateData.ca.push(cert.join('\n'));
                        cert = [ ]
                    }
                })
            }
        }

        callback && callback();
    }
}

module.exports = {
	Client,
	Server,
    Multiplexer,
    Router,
    createFromConfig,
    config,
    Certificates
}


/* eslint-disable operator-linebreak */

const _ = {
  cloneDeep: require('lodash.clonedeep')
}

const defaultMiddleware = require('./default-middleware')

module.exports = function MessengerBodyProcessor (options) {
  const middleware = []
  processMessengerBody.use = use

  const log = (options && options.log) || console
  use(function optionsCustomLogger (message, context) {
    context.log = log
  })

  Object
    .keys(defaultMiddleware)
    .forEach(key => use(defaultMiddleware[key]))

  return processMessengerBody

  /**
   * Process messages from Facebook Messenger.
   *
   * @param  {object}   body               _parsed_ JSON HTTP message body.
   * @param  {function} beforeMiddlewareFn optional function to call before executing
   *                                       any plugins.
   * @return {boolean}                     + `true` if the Message was processed
   *                                       + `false` if the body didn't contain
   *                                          Messenger messages
   */
  async function processMessengerBody (body, context) {
    const allMessages = getAllMessages(body)
    if (!allMessages || !allMessages.length) return false

    context = context || {}

    for (let message of allMessages) {
      message = _.cloneDeep(message)
      const messageContext = Object.assign({}, context)
      try {
        for (let plugin of middleware) {
          await plugin(message, messageContext)
        }
      } catch (error) {
        const logError = (messageContext.log && messageContext.log.error instanceof Function)
          ? messageContext.log.error
          : console.error
        logError('Error running middleware', error)
      }
    }
    return true
  }

  function use (plugin) {
    middleware.push(plugin)
    return processMessengerBody
  }
}

function getAllMessages (body) {
  return body
    && body.object === 'page'
    && Array.isArray(body.entry)
    && body.entry.reduce((allMessages, entry) => {
      if (Array.isArray(entry.messaging)) Array.prototype.push.apply(allMessages, entry.messaging)
      return allMessages
    }, [])
}


/*!
 * base-config <https://github.com/jonschlinkert/base-config>
 *
 * Copyright (c) 2015-2016, Jon Schlinkert.
 * Licensed under the MIT License.
 */

'use strict';

var utils = require('./utils');

/**
 * Expose `config`
 */

module.exports = function() {
  return create('config');
};

/**
 * Create a function for mapping `app` properties onto the
 * given `prop` namespace.
 *
 * @param {String} `prop` The namespace to use
 * @param {Object} `argv`
 * @return {Object}
 * @api public
 */

function create(prop) {
  if (typeof prop !== 'string') {
    throw new Error('expected the first argument to be a string.');
  }

  return function(app) {
    if (this.isRegistered('base-' + prop)) return;

    // map config
    var config = utils.mapper(app)
      // store/data
      .map('store', store(app.store))
      .map('data')

      // options
      .map('enable')
      .map('disable')
      .map('option')
      .alias('options', 'option')

      // get/set
      .map('set')
      .map('del')

    // Expose `prop` (config) on the instance
    app.define(prop, proxy(config));

    // Expose `process` on app[prop]
    app[prop].process = config.process;
  };

  function store(app) {
    if (!app) return {};
    var mapper = utils.mapper(app);
    app.define(prop, proxy(mapper));
    return mapper;
  }
}

/**
 * Proxy to support `app.config` as a function or object
 * with methods, allowing the user to do either of
 * the following:
 *
 * ```js
 * base.config({foo: 'bar'});
 *
 * // or
 * base.config.map('foo', 'bar');
 * ```
 */

function proxy(config) {
  function fn(key, val) {
    if (typeof val === 'string') {
      config.alias.apply(config, arguments);
      return config;
    }

    if (typeof key === 'string') {
      config.map.apply(config, arguments);
      return config;
    }

    if (!utils.isObject(key)) {
      throw new TypeError('expected key to be a string or object');
    }

    for (var prop in key) {
      fn(prop, key[prop]);
    }
    return config;
  }
  fn.__proto__ = config;
  return fn;
}

/**
 * Expose `create`
 */

module.exports.create = create;


module.exports = function ( Class ) {
    var Util = require( 'findhit-util' );

    var origExtend = Class.extend;
    Class.extend = function ( props ) {
        var NewClass = origExtend.apply( this, arguments ),
            proto = NewClass.prototype,
            parentProto = NewClass.parent.prototype;

        if( typeof NewClass !== 'function' ) {
            return NewClass;
        }

        populateProto( proto, parentProto );

        return NewClass;
    };

    // add a constructor hook
    Class.addInitHook = function (fn) { // (Function) || (String, args...)
        var args = Array.prototype.slice.call(arguments, 1);

        var init = typeof fn === 'function' ? fn : function () {
            this[fn].apply(this, args);
        };

        this.prototype._initHooks = this.prototype._initHooks || [];
        this.prototype._initHooks.push(init);
    };

    // add a destruction hook
    Class.addDestroyHook = function ( fn ) { // (Function) || (String, args...)
        var args = Array.prototype.slice.call( arguments, 1 );

        var destroy = typeof fn === 'function' ? fn : function () {
            this[fn].apply(this, args);
        };

        this.prototype._destroyHooks = this.prototype._destroyHooks || [];
        this.prototype._destroyHooks.push(destroy);
    };

    // Private functions
    var populateProto = function ( proto, parentProto ) {

        proto._initHooks = [];
        proto._destroyHooks = [];

        // add method for calling all init hooks
        proto.callInitHooks = function () {

            if ( this._initHooksCalled ) { return; }

            if ( parentProto ) {
                parentProto.callInitHooks.call(this);
            }

            this._initHooksCalled = true;

            for (var i = 0, len = proto._initHooks.length; i < len; i++) {
                proto._initHooks[i].call(this);
            }
        };

        // add method for calling all destroy hooks
        proto.callDestroyHooks = function () {

            if ( this._destroyHooksCalled ) { return; }

            if ( parentProto.callDestroyHooks ) {
                parentProto.callDestroyHooks.call(this);
            }

            this._destroyHooksCalled = true;

            for (var i = 0, len = proto._destroyHooks.length; i < len; i++) {
                proto._destroyHooks[i].call(this);
            }
        };

    };

    // Because class isnt extended, run populateProto on it
    populateProto( Class.prototype, null );

};


var util = require('util');
var commonality = require('./commonality.js');
var CommonalityInterface = commonality.Interface;
var CommanalityMatrix = commonality.Matrix;

//
// Create the exposed class
//
function CommonalityElement(top) {
  CommonalityInterface.call(this, ElementMatrix, top);
}
module.exports = CommonalityElement;
util.inherits(CommonalityElement, CommonalityInterface);

//
// Collects features and bind them to the node density
//
function ElementMatrix(top) {
  CommanalityMatrix.call(this, top);

  this.row(' ');
  this.collum(' ');
  this.classlist = top.root.classlist;
}
util.inherits(ElementMatrix, CommanalityMatrix);

// Create a list of classnames there contains more than one node in the tree
ElementMatrix.prototype._classNames = function (node) {
  var reducedClassnames = [];
  if (node.type === 'element') {
    for (var n = 0, r = node.classes.length; n < r; n++) {
      if (this.classlist.get(node.classes[n]).length > 1) {
        reducedClassnames.push(node.classes[n]);
      }
    }
  }

  return reducedClassnames.length === 0 ? [' '] : reducedClassnames;
};

ElementMatrix.prototype.append = function (node) {
  var tagname = node.type === 'text' ? ' ' : node.tagname;

  // Create a list of classnames there contains more than one node in the tree
  var classnames = this._classNames(node);

  // prepear all classname collums for a new tagname
  var rowId = this.row(tagname);

  for (var i = 0, l = classnames.length; i < l; i++) {
    // Create a new collum in case this is a new tagname
    var collumId = this.collum(classnames[i]);

    // Store the node
    this.nodeMatrix[collumId][rowId].push(node);
  }
};

ElementMatrix.prototype.cellName = function (a, b) {
  return (this.rowNames[b] === ' ' ? 'null' : this.rowNames[b]) +
         ('.') +
         (this.collumNames[a] === ' ' ? 'null' : this.collumNames[b]);
};


/**
 * node-s3-encode-url
 */

function publicS3URI(string) {
	return encodeURIComponent(string)
		.replace(/%20/img, '+')
		.replace(/%2F/img, '/')
    .replace(/\"/img, "%22")
    .replace(/\#/img, "%23")
    .replace(/\$/img, "%24")
    .replace(/\&/img, "%26")
    .replace(/\'/img, "%27")
		.replace(/\(/img, "%28")
    .replace(/\)/img, "%29")
    .replace(/\,/img, "%2C")
    .replace(/\:/img, "%3A")
    .replace(/\;/img, "%3B")
    .replace(/\=/img, "%3D")
    .replace(/\?/img, "%3F")
    .replace(/\@/img, "%40");
}

if(module) {
	module.exports = publicS3URI;
}


module.exports = {

  console: function (message, context) {
    context.log = context.log || console
  },

  messenger: function (message, context) {
    context.messenger = {
      timestamp: message.timestamp,
      sender:    message.sender,
      recipient: message.recipient,
      message
    }
  },

  postback: function postback (message, context) {
    if (!message.postback) return

    const data = message.postback
    context.referral = data.referral
    Object.assign(context, parsePayload(data.payload))
    context.topic = `postback${context.topic ? ('.' + context.topic) : ''}`
  },

  message: function message (message, context) {
    if (!message.message) return

    const data = message.message
    if (data.quick_reply) {
      Object.assign(context, parsePayload(data.quick_reply.payload))
      context.topic = `quick_reply${context.topic ? ('.' + context.topic) : ''}`
      return
    }

    /* istanbul ignore else */
    if (data.text) {
      context.topic = 'text'
      context.data = data.text
    }
  },

  referral: function referral (message, context) {
    if (!message.referral) return

    context.topic = 'referral'
    context.data  = message.referral
  }
}

/**
 * parse a string in the format `topic[:data]`, where `data` is valid JSON
 *
 * @param  {[type]} message a string in the format topic[:data], e.g.
 *                          `nextPage` or `gotoPage:{"pageNumber":2}`
 * @return {[type]} { topic, data }
 */
function parsePayload (message) {
  const messageParts = message && message.match(/^([^:]+)(?::(.*))?/)
  if (!messageParts) return {
    topic: undefined,
    data:  undefined
  }

  const topic = messageParts[1]
  let   data = messageParts[2]

  if (data) {
    try {
      data = JSON.parse(data)
    } catch (error) {
      data = undefined
    }
  }

  return {
    topic,
    data
  }
}


var fs = require('fs');
var path = require('path');
var async = require('async');
var request = require('request');
var startpoint = require('startpoint');

var analyse = require('../../lib/article.js');

var datamap = require('../../test/reallife/datamap.json');
var UNLABLED = datamap.map(function (item, index) {
  return {
    labeled: item.labeled,
    href: item.href,
    key: item.key,
    why: item.why,
    index: index
  };
}).filter(function (item) {
  return !item.labeled;
});

//
// Lots of sideeffects :)
//
var OPEN_FLAG = false;
var SKIPS = 0;
var SOURCES = {};

function Logic(ws) {
  this.ws = ws;
}
module.exports = Logic;

Logic.prototype.send = function (msg) {
  this.ws.send(JSON.stringify(msg));
};

Logic.prototype._requestLabel = function () {
  var self = this;

  if (UNLABLED.length <= SKIPS) {
    this.send({ 'what': 'done', 'data': SKIPS });
  } else {
    var item = UNLABLED[SKIPS];
    this.send({ 'what': 'label-processing', 'data': item });

    this._analyse(item, function (err, result) {
      if (err) return self.send({ 'what': 'error', 'data': err.message });

      self.send({ 'what': 'label-analysed', 'data': result });
    });
  }
};

Logic.prototype._analyse = function (item, callback) {
  request({url: item.href, jar: true}, function (err, res, body) {
    if (err) return callback(err);
    if (res.statusCode !== 200) return callback(new Error('Status code was ' + res.statusCode));
    if (body.length === 0) return callback(new Error('No body was send'));

    SOURCES[item.key] = body;
    startpoint(body).pipe(analyse(item.href, callback));
  });
};

Logic.prototype.open = function () {
  if (OPEN_FLAG === true) {
    this.send({ 'what': 'error', 'data': 'already open' });
  } else {
    OPEN_FLAG = true;
    this._requestLabel();
  }
};

Logic.prototype.close = function () {
  OPEN_FLAG = false;
};

var HANDLERS = {
  skip: function (item) {
    SKIPS += 1;
    delete SOURCES[item.key];
    this._requestLabel();
  },

  save: function (item) {
    var self = this;

    this.send({ 'what': 'label-saving' });

    async.series([
      // Save expected file
      function (done) {
        fs.writeFile(
          path.resolve(__dirname, '../../test/reallife/expected/' + item.key + '.json'),
          JSON.stringify({
            'title': item.title,
            'text': item.text
          }, null, '\t') + '\n',
          done
        );
      },

      // Save source file
      function (done) {
        fs.writeFile(
          path.resolve(__dirname, '../../test/reallife/source/' + item.key + '.html'),
          SOURCES[item.key],
          done
        );
      },

      // Save labeled flag
      function (done) {
        datamap[item.index].labeled = true;

        fs.writeFile(
          path.resolve(__dirname, '../../test/reallife/datamap.json'),
          JSON.stringify(datamap, null, '\t') + '\n',
          done
        );
      }
    ], function (err) {
      if (err) return self.send({ 'what': 'error', 'data': err.message });

      // Cleanup memory
      delete SOURCES[item.key];
      UNLABLED.splice(SKIPS, 1);

      self._requestLabel();
    });
  }
};

Logic.prototype.message = function (msg) {
  if (HANDLERS.hasOwnProperty(msg.what) === false) {
    this.send({ 'what': 'error', 'data': 'no such command' });
  } else {
    HANDLERS[msg.what].call(this, msg.data);
  }
};


var nodes = require('../nodes.js');
var domHelpers = require('../helpers-dom.js');

//
// Mutate the DOM of the best text container so only the article text is left
//
function Subtree(node) {
  this.top = node;
}
module.exports = Subtree;

Subtree.prototype.findImages = function () {
  var images = [];

  (function recursive(node) {
    for (var i = 0, l = node.children.length; i < l; i++) {
      var subnode = node.children[i];
      if (subnode.type === 'text') continue;

      if (subnode.tagname === 'img' || subnode.tagname === 'object') {
        images.push(subnode);
      } else {
        recursive(subnode);
      }
    }
  })(this.top);

  return images;
};

Subtree.prototype.findTextNodes = function () {
  var nodes = [];

  (function recursive(node) {
    for (var i = 0, l = node.children.length; i < l; i++) {
      var subnode = node.children[i];

      if (subnode.type === 'text') {
        nodes.push(subnode);
      } else {
        recursive(subnode);
      }
    }
  })(this.top);

  return nodes;
};

Subtree.prototype.surroundingBrElems = function (node) {
  var beforeCount = 0;
  var beforeElem = node;
  while((beforeElem = beforeElem.before()) &&
        beforeElem.type === 'element' && beforeElem.tagname === 'br') {
    beforeCount += 1;
  }

  var afterCount = 0;
  var afterElem = node;
  while((afterElem = afterElem.after()) &&
        afterElem.type === 'element' && afterElem.tagname === 'br') {
    afterCount += 1;
  }

  return beforeCount + afterCount;
};

Subtree.prototype.surroundingBlockElem = function (node) {
  var before, beforeNode = node;
  while (before = beforeNode.before()) {
    if (before.type === 'element' &&
        domHelpers.BLOCK_ELEMENTS.hasOwnProperty(before.tagname)) {
      return true;
    }

    if (before !== beforeNode.parent) break;
    beforeNode = before;
  }

  var after, afterNode = node;
  while (after = afterNode.before()) {

    if (after.type === 'element' &&
        domHelpers.BLOCK_ELEMENTS.hasOwnProperty(after.tagname)) {
      return true;
    }

    if (after !== afterNode.parent) break;
    afterNode = after;
  }

  return false;
};

function hasTextChild(node) {
  for (var i = 0, l = node.children.length; i < l; i++) {
    if (node.children[i].type === 'text') return true;
  }
  return false;
}

// This method reduces the tree so only the important elements are left
Subtree.prototype.reduceTree = function () {
  var self = this;

  if (this.top.children.length === 0) return;

  (function recursive(node) {
    // Skip element if its already removed
    if (node.inTree === false) { }

    // Cleanup text nodes
    else if (node.type === 'text') {
      // Remove empty text nodes
      if (node.getText().trim() === '') {
        node.remove();
      }
      else {
        var afterNode = node.after();
        if (afterNode.type === 'text') {
          node.append(afterNode);
          node.close();
          afterNode.remove();
        }
      }
    }
    // Cleanup elements
    else if (node.type === 'element') {
      var children = node.children.slice(0);
      for (var i = 0, l = children.length; i < l; i++) {
        recursive(children[i]);
      }

      // move  and <object> nodes to the nearest block container
      if (node.tagname === 'img' || node.tagname === 'object') {
        var destination = node;
        while (destination = destination.parent) {
          if (destination === self.top || (
              domHelpers.BLOCK_ELEMENTS.hasOwnProperty(destination.tagname) &&
              hasTextChild(destination) === false)
          ) {
            break;
          }
        }

        if (destination !== node.parent) {
          node.remove();
          destination.insert(node, 0);
        }
      }
      // Reduce
 tags to the level there matters
      else if (node.tagname === 'br') {
        // Remove this br tag if there are enoght
 tags or there is a
        // surrounding block element
        if (self.surroundingBrElems(node) >= 2 || self.surroundingBlockElem(node)) {
          node.remove();
        }
      }
      // elements with no children, can most likely just be removed or replaced
      else if (node.children.length === 0) {
        // Replace empty block elements with br tags
        if (domHelpers.BLOCK_ELEMENTS.hasOwnProperty(node.tagname)) {
          // Figure how many br tags there should be created and create them
          if (self.surroundingBlockElem(node) === false) {
            var brs = Math.max(0, 2 - self.surroundingBrElems(node));
            var index = node.parent.children.indexOf(node);
            for (var b = 0; b < brs; b++) {
              var br = new nodes.ElementNode(node.parent, 'br', {});
                  br.close();
              node.parent.children.splice(index, 0, br);
            }
          }
        }
        // Concat textnodes if possible
        else {
          var before = node.before();
          var after = node.after();
          if (before.type === 'text' && after.type === 'text') {
            before.append(after);
            before.close();
            after.remove();
          }
        }

        // Now remove the node, there don't contain anything
        node.remove();
      }
      // If this is an style element with only a text node as a child
      else if (domHelpers.INLINE_TEXT_TAGS.hasOwnProperty(node.tagname) &&
               node.children.length === 1 && node.children[0].type === 'text') {
        var textnode = node.children[0];
        var mergenode = null;

        var before = node.before();
        var after = node.after();

        // Find or create the text node that the child should be merge intro
        if (before.type === 'text') {
          mergenode = before;
        } else if (after.type === 'text') {
          mergenode = new nodes.TextNode(node.parent, '');
          var mergeInsert = node.parent.children.indexOf(after);
          node.parent.children.splice(mergeInsert, 0, mergenode);
        }

        // If a mergenode was found, append the textnode and if possible the
        // after node too.
        if (mergenode !== null) {
          mergenode.append(textnode);
          node.remove();

          if (after.type === 'text') {
            mergenode.append(after);
            after.remove();
          }

          mergenode.close();
        }
      }
    }
  })(this.top);
};

// Grap and create container nodes
Subtree.prototype.containerNodes = function () {
  var containers = [];

  (function recursive(node) {
    for (var i = 0, l = node.children.length; i < l; i++) {
      var subnode = node.children[i];

      if ((subnode.blocky === false ||
          (subnode.blocky === true && subnode.blockyChildren === false)) &&
          subnode.density !== 0) {
        containers.push(subnode);
      } else {
        recursive(subnode);
      }
    }
  })(this.top);

  return containers;
};

//
// A secret little gem, you can use to print out the subtree :)
//
function attrStringify(attr) {
  var names = Object.keys(attr);
  var str = '';
  for (var i = 0, l = names.length; i < l; i++) {
    str += names[i] + '="' + attr[names[i]].slice(0, 20) + (attr[names[i]].length > 20 ? '...' : '') + '" ';
  }
  return str;
}

Subtree.prototype.print = function () {
  var printout = '';
  var containers = this.containerNodes();

  (function recursive(indent, node) {
    var blocky = containers.indexOf(node) !== -1;

    printout += indent + (blocky ? '\x1B[33m' : '');
    printout += node.print();
    printout += '\n' + (blocky ? '\x1B[39m' : '');

    if (node.type === 'element') {
      for (var i = 0, l = node.children.length; i < l; i++) {
        recursive(indent + '  ', node.children[i]);
      }
    }
  })('', this.top);

  return printout;
};


'use strict'

var vfileLocation = require('vfile-location')
var toString = require('nlcst-to-string')
var position = require('unist-util-position')
var phrasing = require('hast-util-phrasing')
var embedded = require('hast-util-embedded')
var whitespace = require('hast-util-whitespace')
var textContent = require('hast-util-to-string')
var is = require('hast-util-is-element')

module.exports = toNLCST

// Elements representing source.
var SOURCE = ['code']
var IGNORE = ['script', 'style', 'svg', 'math', 'del']
var EXPLICIT = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']

// Constants.
var FLOW_ACCEPTING = [
  'body',
  'article',
  'section',
  'blockquote',
  'nav',
  'aside',
  'header',
  'footer',
  'address',
  'li',
  'dt',
  'dd',
  'figure',
  'figcaption',
  'div',
  'main',
  'caption',
  'td',
  'th',
  'form',
  'fieldset',
  'details',
  'dialog'
]

// Transform `tree` to `nlcst`.
function toNLCST(tree, file, Parser) {
  var parser
  var location
  var results
  var doc

  // Warn for invalid parameters.
  if (!tree || !tree.type) {
    throw new Error('hast-util-to-nlcst expected node')
  }

  if (!file || !file.messages) {
    throw new Error('hast-util-to-nlcst expected file')
  }

  // Construct parser.
  if (!Parser) {
    throw new Error('hast-util-to-nlcst expected parser')
  }

  if (!position.start(tree).line || !position.start(tree).column) {
    throw new Error('hast-util-to-nlcst expected position on nodes')
  }

  location = vfileLocation(file)
  doc = String(file)
  parser = 'parse' in Parser ? Parser : new Parser()

  // Transform HAST into NLCST tokens, and pass these into `parser.parse` to
  // insert sentences, paragraphs where needed.
  results = []

  find(tree)

  return {
    type: 'RootNode',
    children: results,
    position: {
      start: location.toPosition(0),
      end: location.toPosition(doc.length)
    }
  }

  function find(node) {
    var children = node.children

    if (node.type === 'root') {
      findAll(children)
    } else if (is(node) && !ignored(node)) {
      if (is(node, EXPLICIT)) {
        // Explicit paragraph.
        add(node)
      } else if (is(node, FLOW_ACCEPTING)) {
        // Slightly simplified version of:
        // https://html.spec.whatwg.org/#paragraphs
        implicit(flattenAll(children))
      } else {
        // Dig deeper.
        findAll(children)
      }
    }
  }

  function findAll(children) {
    var length = children.length
    var index = -1

    while (++index < length) {
      find(children[index])
    }
  }

  function flatten(node) {
    if (is(node, ['a', 'ins', 'del', 'map'])) {
      return flattenAll(node.children)
    }

    return node
  }

  function flattenAll(children) {
    var results = []
    var length = children.length
    var index = -1

    while (++index < length) {
      results = results.concat(flatten(children[index]))
    }

    return results
  }

  function add(node) {
    var result = ('length' in node ? all : one)(node)

    if (result.length !== 0) {
      results.push(parser.tokenizeParagraph(result))
    }
  }

  function implicit(children) {
    var length = children.length + 1
    var index = -1
    var viable = false
    var start = -1
    var child

    while (++index < length) {
      child = children[index]

      if (child && phrasing(child)) {
        if (start === -1) {
          start = index
        }

        if (!viable && !embedded(child) && !whitespace(child)) {
          viable = true
        }
      } else if (child && start === -1) {
        find(child)
        start = index + 1
      } else {
        ;(viable ? add : findAll)(children.slice(start, index))

        if (child) {
          find(child)
        }

        viable = false
        start = -1
      }
    }
  }

  // Convert `node` (hast) to nlcst.
  function one(node) {
    var type = node.type
    var tagName = type === 'element' ? node.tagName : null
    var change
    var replacement

    if (type === 'text') {
      change = true
      replacement = parser.tokenize(node.value)
    } else if (tagName === 'wbr') {
      change = true
      replacement = [parser.tokenizeWhiteSpace(' ')]
    } else if (tagName === 'br') {
      change = true
      replacement = [parser.tokenizeWhiteSpace('\n')]
    } else if (sourced(node)) {
      change = true
      replacement = [parser.tokenizeSource(textContent(node))]
    } else if (type === 'root' || !ignored(node)) {
      replacement = all(node.children)
    } else {
      return
    }

    if (!change) {
      return replacement
    }

    return patch(replacement, location, location.toOffset(position.start(node)))
  }

  // Convert all `children` (HAST) to NLCST.
  function all(children) {
    var length = children && children.length
    var index = -1
    var result = []
    var child

    while (++index < length) {
      child = one(children[index])

      if (child) {
        result = result.concat(child)
      }
    }

    return result
  }

  // Patch a position on each node in `nodes`.  `offset` is the offset in
  // `file` this run of content starts at.
  //
  // Note that NLCST nodes are concrete, meaning that their starting and ending
  // positions can be inferred from their content.
  function patch(nodes, location, offset) {
    var length = nodes.length
    var index = -1
    var start = offset
    var children
    var node
    var end

    while (++index < length) {
      node = nodes[index]
      children = node.children

      if (children) {
        patch(children, location, start)
      }

      end = start + toString(node).length

      node.position = {
        start: location.toPosition(start),
        end: location.toPosition(end)
      }

      start = end
    }

    return nodes
  }
}

function sourced(node) {
  var props = node.properties
  return is(node) && (is(node, SOURCE) || props.dataNlcst === 'source')
}

function ignored(node) {
  var props = node.properties
  return is(node) && (is(node, IGNORE) || props.dataNlcst === 'ignore')
}


module.exports = function ( Class ) {
    var Util = require( 'findhit-util' );

    /*
     * N:M :
     *  ClassA.hasMany( ClassB, 'B' );
     *  ClassB.hasMany( ClassA, 'A' );
     *
     * 1:M :
     *  ClassA.hasMany( ClassB, 'B' );
     *  ClassB.belongsTo( ClassA, 'A' );
     *
     * 1:1 :
     *  ClassA.hasOne( ClassB, 'B' );
     *  ClassB.belongsTo( ClassA, 'A' );
     *
     */

    Class.hasMany = function ( alias, TargetClass ) {
        return this.associate( alias, TargetClass, Infinity );
    };

    Class.hasOne = function ( alias, TargetClass ) {
        return this.associate( alias, TargetClass, 1 );
    };

    Class.belongsTo = function ( alias, TargetClass ) {
        return this.associate( alias, TargetClass, 1 );
    };

    // Backbone of association

    Class.associate = function ( alias, TargetClass, type ) {
        var association = this.associations.get( TargetClass );

        if ( association ) {
            return false;
        }

        return new Association( this, TargetClass, alias, type );
    };

    Class.deassociate = function ( TargetClass ) {
        var association = this.associations.get( TargetClass );

        if ( ! association ) {
            return false;
        }

        association.unbind();

        if( association.targetAssocation ) {
            association.targetAssociation.unbind();
            association.unbindTargetAssociation();
        }

        return true;
    };

    // Associations

    var Association = function ( source, target, alias, type ) {

        if (
            Util.isnt.Class( source ) ||
            Util.isnt.Class( target ) ||
            Util.isnt.String( alias ) || ! alias ||
            Association.types.indexOf( type ) === -1
        ) {
            return false;
        }

        this.id = Util.uniqId();
        this.source = source;
        this.target = target;
        this.alias = Util.String.capitalize( alias );
        this.type = type;

        return this.complete();
    };

    // Implementation into Class
    var origExtend = Class.extend;
    Class.extend = function ( props ) {
        var NewClass = origExtend.apply( this, arguments ),
            parent = NewClass.parent.prototype;

        if ( typeof NewClass !== 'function' ) {
            return NewClass;
        }

        // Create associations prototyped from parentProto or a new object
        var associations = parent && parent.associations ? ClassUtil.create( parent.associations ) : {};

        Object.defineProperty( NewClass, 'associations', {
            enumerable: false,
            writable: true,
            value: associations,
        });

        // Inject a protected getter
        Object.defineProperty( associations, "get", {
            enumerable: false,
            writable: false,
            value: Association._get.bind( associations ),
        });

        return NewClass;
    };

    Association._get = function ( from ) {
        var filtered;

        if ( ! from ) {
            return false;
        }

        // Check if it is an Alias
        if ( Util.is.String( from ) ) {
            return this[ from ] || false;
        }

        // Check for target
        if ( Util.is.Class( from ) ) {
            filtered = Util.filter( this, function ( association ) {
                return association.target === from;
            });

            return filtered[ Object.keys( filtered )[ 0 ] ] || false;
        }

        // Check for association
        if ( Util.is.instanceof( Association, from ) ) {
            filtered = Util.filter( this, function ( association ) {
                return association === from || association.targetAssociation === from;
            });

            return filtered[ Object.keys( filtered )[ 0 ] ] || false;
        }

        // In last case...
        return false;
    };

    Association.types = [
        1,
        Infinity
    ];

    Association.prototype.complete = function () {
        if( this.completed ) return this;

        this.bind();
        this.bindTargetAssociation();

        if( ! this.targetAssociation ) {
            return this;
        }

        this.completed = true;

        // Try to complete again targetAssociation
        this.targetAssociation.complete();

        // Inject this side getter and setter
        this.injectSourceGetterAndSetter();

        return this;
    };

    Association.prototype.bindTargetAssociation = function () {
        this.targetAssociation = this.target.associations.get( this.source );

        if ( this.targetAssociation ) {
            this.targetAssociation.targetAssociation = this;
        }
    };

    Association.prototype.unbindTargetAssociation = function () {

        if ( this.targetAssociation ) {
            this.targetAssociation.targetAssociation = null;
        }

        this.targetAssociation = false;
    };

    Association.prototype.bind = function () {
        this.source.associations[ this.alias ] = this;
    };

    Association.prototype.unbind = function () {
        delete this.source.associations[ this.alias ];
    };

    Association.prototype.injectSourceGetterAndSetter = function () {
        if ( this.alreadyInjected ) {
            return false;
        }

        var association = this,
            resetter, getter, setter, adder, remover;

        switch( this.type ) {

            case 1:

                resetter = this.source.prototype[ 'reset' + this.alias ] = function () {
                    var connector = this.connectors.get( association );

                    connector.clear();

                    return true;
                };

                getter = this.source.prototype[ 'get' + this.alias ] = function () {
                    var connector = this.connectors.get( association );

                    return connector.get( 0 );
                };

                setter = this.source.prototype[ 'set' + this.alias ] = function ( instance ) {
                    if ( ! instance || Util.isnt.instanceof( association.target, instance ) ) {
                        return false;
                    }

                    // Grab connector and remoteConnector
                    var connector = this.connectors.get( association ),
                        remoteConnector = instance.connectors.get( association.targetAssociation );

                    // Try to arrange this into remote connector
                    if ( remoteConnector ) {
                        if( remoteConnector.association.type === 1 ) {
                            remoteConnector.clear();
                        }

                        remoteConnector.add( this );
                    }

                    connector.clear();
                    return connector.add( instance );
                };

            break;

            case Infinity:

                resetter = this.source.prototype[ 'reset' + this.alias ] = function () {
                    var connector = this.connectors.get( association );

                    connector.clear();

                    return true;
                };

                getter = this.source.prototype[ 'get' + this.alias ] = function ( i ) {
                    var connector = this.connectors.get( association );

                    return connector.get( i );
                };

                setter = this.source.prototype[ 'set' + this.alias ] = function ( instances ) {
                    var self = this;

                    // HEY, CHECK THIS FUCKING OUT!!!
                    // Easy way, remove all current instances by running get with current instance binded
                    (remover.bind( self ))( (getter.bind( self ))() || [] );

                    // Then map instances we want to set and run add with current instance binded
                    return (adder.bind( self ))( instances );
                };

                adder = this.source.prototype[ 'add' + this.alias ] = function ( instances ) {
                    var self = this;
                    return Util.map( instances, function ( instance ) {
                        if ( ! instance || Util.isnt.instanceof( association.target, instance ) ) {
                            return false;
                        }

                        // Grab connector and remoteConnector
                        var connector = self.connectors.get( association ),
                            remoteConnector = instance.connectors.get( association.targetAssociation );

                        // Try to arrange this into remote connector
                        if ( remoteConnector ) {
                            if( remoteConnector.association.type === 1 ) {
                                remoteConnector.clear();
                            }

                            remoteConnector.add( self );
                        }

                        return connector.add( instance );
                    });
                };

                remover = this.source.prototype[ 'remove' + this.alias ] = function ( instances ) {
                    var self = this;
                    return Util.map( instances, function ( instance ) {
                        if ( ! instance || Util.isnt.instanceof( association.target, instance ) ) {
                            return false;
                        }

                        // Grab connector and remoteConnector
                        var connector = self.connectors.get( association ),
                            remoteConnector = instance.connectors.get( association.targetAssociation );

                        // Try to arrange this into remote connector
                        if ( remoteConnector ) {
                            if( remoteConnector.association.type === 1 ) {
                                remoteConnector.clear();
                            } else {
                                remoteConnector.remove( self );
                            }
                        }

                        return connector.remove( instance );
                    });
                };

            break;

        }

        // Now lets set global things :)

        association.source.addInitHook(function () {
            this.connectors[ association.alias ] = new Connector( association );

            // Property binding
            Object.defineProperty( this, association.alias, {
                enumerable: true,
                get: getter,
                set: setter,
            });

        });

        this.alreadyInjected = true;
        return true;
    };

    Association.prototype.removeSourceGettersAndSetters = function () {
        if ( ! this.alreadyInjected ) {
            return false;
        }

        this.alreadyInjected = false;
        return true;
    };

    // Connectors

    var Connector = function ( association ) {
        this.association = association;
        this.array = [];

        return this;
    };

    // Implementation into Class
    Class.addInitHook(function () {
        var connectors = {};

        Object.defineProperty( this, "connectors", {
            enumerable: false,
            writable: true,
            value: connectors,
        });

        // Inject a protected getter
        Object.defineProperty( connectors, "get", {
            enumerable: false,
            writable: false,
            value: Connector._get.bind( connectors ),
        });
    });

    Connector._get = function ( from ) {
        var filtered;

        // This should be used inside a connectors context, not Connector's Fn
        if( this === Connector ) {
            return false;
        }

        if ( ! from ) {
            return false;
        }

        // Check if it is an Alias
        if ( Util.is.String( from ) ) {
            return this[ from ] || false;
        }

        // Check for target
        if ( Util.is.Class( from ) ) {
            filtered = Util.filter( this, function ( connector ) {
                return connector.association.target === from;
            });

            return filtered[ Object.keys( filtered )[ 0 ] ] || false;
        }

        // Check for association
        if ( Util.is.instanceof( Association, from ) ) {
            filtered = Util.filter( this, function ( connector ) {
                return connector.association === from;
            });

            return filtered[ Object.keys( filtered )[ 0 ] ] || false;
        }

        // In last case...
        return false;
    };

    Connector.prototype.clear = function () {

        if ( this.array.length ) {
            this.array.splice( 0, this.array.length );
        }

        return true;
    };

    Connector.prototype.get = function ( i ) {

        // Force i if this is a only one
        if ( this.association.type === 1 ) {
            // Sorry, i whould be 0
            i = 0;
        }

        if ( Util.is.undefined( i ) ) {
            return this.array;
        }

        return this.array[ i ] || null;
    };

    Connector.prototype.add = function ( instance ) {

        // Dont add if it is currently present
        if ( this.array.indexOf( instance ) !== -1 ) {
            return false;
        }

        // Dont add if this is a One-to-something connection and we already have some
        if ( this.association.type === 1 && this.array.length ) {
            return false;
        }

        this.array.push( instance );

        return true;
    };

    Connector.prototype.remove = function ( i ) {

        // Accept a number as index, otherwise try to gess it by checking if it is present into the array
        i = Util.is.number( i ) && i > -1 && i < this.array.length ? i : this.array.indexOf( i );

        if ( i === -1 ) {
            return false;
        }

        this.array.splice( i, 1 );

        return true;
    };

};


var util = require('util');
var domHelpers = require('./helpers-dom.js');

var WHITE_SPACE = /\s+/g;
var NEWLINE_NORMALIZE = /([ ]*)\n([ ]*)/g;
var DOUBLE_NEWLINE_NORMALIZE = /([\n]*)\n\n([\n]*)/g;

//
// Simple DOM:
// > like object structure, this is what they have in common
//  - `.type` is a string (root, text, element)
//  - `.parent` refer to the nodes parrent
//  - `.identifyer` is a simply id used as an object property name elsewhere
//  - `.children` is an array of the elements children
//  - `.append` adds a node to this node
//  - `.element` return the actual element
//

//
// Node
//  General abstaction between all node types
//  The rule is that this should contain the most obviouse implementation
//
function Node(type, parent) {
  this.type = type;
  this.parent = parent;
  this.root = parent ? parent.root : this;
  this.identifyer = parent ? (++parent.root._counter) : 0;

  // The specific constructor will set another value if necessary
  this._textLength = 0;

  this.tags = 0;
  this.density = -1;

  this.children = [];

  this._text = '';
  this._textCompiled = false;
  this._noneStyleText = '';
  this._noneStyleTextCompiled = false;

  this.blocky = false;
  this.blockyChildren = false;

  this.inTree = true;
}

// Calculate the internal subStringLength and subTags properties
Node.prototype._updateCounters = function () {
  if (domHelpers.NO_TEXT_ELEMENTS.hasOwnProperty(this.tagname)) return;

  for (var i = 0, l = this.children.length; i < l; i++) {
    this._textLength += this.children[i]._textLength;
    this.tags += this.children[i].tags;
  }
};

// Calculate the density
Node.prototype._updateDensity = function () {
  this.density = (this.tags === 0) ?
    this._textLength : this._textLength / this.tags;
};

Node.prototype.getText = function () {
  if (this._textCompiled === true) return this._text;

  for (var i = 0, l = this.children.length; i < l; i++) {
    var subnode = this.children[i];

    if (subnode.type === 'text') {
      this._text += this.children[i]._text;
    }
    else if (subnode.type === 'element' && subnode.tagname === 'br') {
      this._text += '\n';
    }
    else if (subnode.type === 'element' &&
            domHelpers.BLOCK_ELEMENTS.hasOwnProperty(subnode.tagname) === true) {
      this._text += '\n\n' + subnode.getText() + '\n\n';
    }
    else if (domHelpers.NO_TEXT_ELEMENTS.hasOwnProperty(subnode.tagname) === false) {
      this._text += subnode.getText();
    }
  }

  this._text = this._text
    // Make sure that space don't surround newline
    .replace(NEWLINE_NORMALIZE, '\n')
    // Make sure that there is no more than two newlines after each other
    .replace(DOUBLE_NEWLINE_NORMALIZE, '\n\n');

  this._textCompiled = true;

  return this._text;
};

Node.prototype.getNoneStyledText = function () {
  if (this._noneStyleTextCompiled === true) return this._noneStyleText;

  for (var i = 0, l = this.children.length; i < l; i++) {
    var subnode = this.children[i];

    if (subnode.type === 'text') {
      this._noneStyleText += this.children[i]._text;
    }
    else if (subnode.tagname === 'br') {
      this._noneStyleText += '\n';
    }
    else if (domHelpers.INLINE_STYLE_TAGS.hasOwnProperty(subnode.tagname) === true) {
      // Skip style tags
    }
    else if (domHelpers.BLOCK_ELEMENTS.hasOwnProperty(subnode.tagname) === true) {
      this._noneStyleText += '\n\n' + subnode.getNoneStyledText() + '\n\n';
    }
    else if (domHelpers.NO_TEXT_ELEMENTS.hasOwnProperty(subnode.tagname) === false) {
      this._noneStyleText += subnode.getNoneStyledText();
    }
  }

  this._noneStyleText = this._noneStyleText
    // Make sure that space don't surround newline
    .replace(NEWLINE_NORMALIZE, '\n')
    // Make sure that there is no more than two newlines after each other
    .replace(DOUBLE_NEWLINE_NORMALIZE, '\n\n');

  this._noneStyleTextCompiled = true;

  return this._noneStyleText;
};

// Set up all the parents to recalculate the text
Node.prototype._textChanged = function () {
  if (this._textLength !== 0) {
    var parent = this;
    while (parent = parent.parent) {
      if (parent._textCompiled === false) break;
      parent._textCompiled = false;
      parent._text = '';
    }
  }
};

// Update blocky flags
Node.prototype._blockyChanged = function () {
  if (this.blocky === true) {
    var parent = this;
    while(parent = parent.parent) {
      parent._updateBlocky();
      // If parent is blocky then nothing at this level has changed
      // and so other parents also won't change.
      if (parent.blocky === true) break;
    }
  }
};

Node.prototype._inTreeFlag = function (bool) {
  this.inTree = bool;
  if (this.type === 'element') {
    for (var i = 0, l = this.children.length; i < l; i++) {
      this.children[i]._inTreeFlag(bool);
    }
  }
};

Node.prototype.remove = function () {
  if (this.parent) {
    // Remove this node
    var index = this.parent.children.indexOf(this);
    if (index !== -1) {
      this.parent.children.splice(index, 1);
    }

    // Update flags
    this._textChanged();
    this._blockyChanged();
    this._inTreeFlag(false);

    // Remove this node from the classlist
    if (this.classes) {
      this.root.classlist.removeNode(this);
    }
  }
};

Node.prototype.insert = function (node, index) {
  // Insert node
  node.parent = this;
  this.children.splice(index, 0, node);

  // Update flags
  this._textChanged();
  this._blockyChanged();

  // Insert this node intro the classlist
  if (this.classes) {
    this.root.classlist.addNode(this);
  }
};

Node.prototype.before = function () {
  var index = this.parent.children.indexOf(this);
  if (index === 0) return this.parent;
  return this.parent.children[index - 1];
};

Node.prototype.after = function () {
  var index = this.parent.children.indexOf(this);
  if (index === this.parent.children.length - 1) return this.parent;
  return this.parent.children[index + 1];
};

Node.prototype._updateBlocky = function () {
	this.blocky = this._blockySelfCache;
	this.blockyChildren = false;

  // Check if a none blocky element contains a blocky element
	for (var i = 0, l = this.children.length; i < l; i++) {
		if (this.children[i].blocky === true) {
			this.blocky = true;
			this.blockyChildren = true;
			break;
		}
	}
};

//
// RootNode
//   has no parent, tagname or attribute
//
function RootNode() {
  Node.call(this, 'root', null);
  this._blockySelfCache = true;
  this._counter = 0;

  this.classlist = new ClassList();
}
util.inherits(RootNode, Node);
exports.RootNode = RootNode;

RootNode.prototype.append = function (child) {
  this.children.push(child);

  return child;
};

RootNode.prototype.close = function () {
  this._updateCounters();
  this._updateDensity();
  this._updateBlocky();

  return null;
};

RootNode.prototype.element = function () {
  return this;
};

RootNode.prototype.print = function () {
  return '!root';
};

//
// FragmentNode
//   has parent and children, but no tagname or attribute
//
function FragmentNode(parent) {
  Node.call(this, 'fragment', parent);
}
util.inherits(FragmentNode, Node);
exports.FragmentNode = FragmentNode;

FragmentNode.prototype.append = function (child) {
  this.children.push(child);

  return child;
};

FragmentNode.prototype.close = function () {
  return this.parent;
};

FragmentNode.prototype.element = function () {
  return this;
};

FragmentNode.prototype.assign = function (node) {
  node.children = this.children;
};

FragmentNode.prototype.print = function () {
  return '!fragment';
};

//
// TextNode
//   has a parent and a text containter
//
function TextNode(parent, text) {
  Node.call(this, 'text', parent);

  // A text node has no children instead it has a text container
  this.children = null;
  this._text = text;
  this._noneStyleText = text;
}
util.inherits(TextNode, Node);
exports.TextNode = TextNode;

TextNode.prototype.append = function (node) {
  if (node.type === 'text') {
    this._text += node._text;
    return this;
  } else {
    // Since the text there are to follow no will not be appended to this text
    // node close it
    this._textClose();

    // the parrent will assign a position
    this.parent.append(node);
    return node;
  }
};

TextNode.prototype._textClose = function () {
  this._text = this._text.replace(WHITE_SPACE, ' ');
  this._textCompiled = true;

  this._textLength = this._text.length;
  this._updateDensity();
};

TextNode.prototype.close = function () {
  this._textClose();

  return this.parent.close();
};

TextNode.prototype.element = function () {
  return this.parent;
};

TextNode.prototype.countTagname = function () {
  return 0;
};

TextNode.prototype.print = function () {
  var text = this._text.trim();
  return '# ' + text.slice(0, 50).trim() + (text.length > 50 ? '...' : '');
};

//
// ElementNode
//   has a parent, tagname and attributes
//

function ElementNode(parent, tagname, attributes) {
  Node.call(this, 'element', parent);

  // Since this is an element there will minimum one tag
  this.tags = (tagname === 'br' || tagname === 'wbr') ? 0 : 1;

  // Element nodes also has a tagname and an attribute collection
  this.tagname = tagname;
  this.attr = attributes;
  this.classes = attributes.hasOwnProperty('class') ?
    attributes['class'].trim().split(WHITE_SPACE) : [];

  // Add node to the classlist
  this.root.classlist.addNode(this);

  this._blockySelfCache = domHelpers.BLOCK_ELEMENTS.hasOwnProperty(tagname);
  this._countTagnames = {};
}
util.inherits(ElementNode, Node);
exports.ElementNode = ElementNode;

ElementNode.prototype.append = function (child) {
  this.children.push(child);

  return child;
};

ElementNode.prototype.close = function () {
  this._updateCounters();
  this._updateDensity();
  this._updateBlocky();

  return this.parent;
};

ElementNode.prototype.element = function () {
  return this;
};

ElementNode.prototype.countTagname = function (tagname) {
  if (this._countTagnames.hasOwnProperty(tagname)) return this._countTagnames[tagname];

  var total = this.tagname === tagname ? 1 : 0;
  for (var i = 0, l = this.children.length; i < l; i++) {
    total += this.children[i].countTagname(tagname);
  }

  this._countTagnames[tagname] = total;
  return total;
};

ElementNode.prototype.print = function () {
  var names = Object.keys(this.attr);
  var str = '';
  for (var i = 0, l = names.length; i < l; i++) {
    var attrStr = this.attr[names[i]];
    str += names[i] + '="' + attrStr.slice(0, 20) + (attrStr.length > 20 ? '...' : '') + '" ';
  }

  return '<' + this.tagname + ' ' + str + '>';
};

//
// Classlist collection
//
function ClassList() {
  this.classes = [];
  this.collections = Object.create(null);
}

ClassList.prototype.get = function (name) {
  return Object.prototype.hasOwnProperty.call(this.collections, name) ?
    this.collections[name] : [];
};

ClassList.prototype.count = function (name) {
  return Object.prototype.hasOwnProperty.call(this.collections, name) ?
    this.collections[name].length : 0;
};

ClassList.prototype.set = function (name, node) {
  if (Object.prototype.hasOwnProperty.call(this.collections, name)) {
    this.collections[name].push(node);
  } else {
    this.collections[name] = [node];
    this.classes.push(name);
  }
};

ClassList.prototype.remove = function (name, node) {
  if (Object.prototype.hasOwnProperty.call(this.collections, name)) {
    var index = this.collections[name].indexOf(node);
    if (index !== -1) {
      this.collections[name].splice(index, 1);
    }

    if (this.collections[name].length === 0) {
      delete this.collections[name];
      this.classes.splice(this.classes.indexOf(name), 1);
    }
  }
};

ClassList.prototype.addNode = function (node) {
  for (var i = 0, l = node.classes.length; i < l; i++) {
    this.set(node.classes[i], node);
  }
};

ClassList.prototype.removeNode = function (node) {
  for (var i = 0, l = node.classes.length; i < l; i++) {
    this.remove(node.classes[i], node);
  }
};


module.exports = function ( Class ) {
    var Util = require( 'findhit-util' ),
        ClassUtil = require( '../util' );

    Class.extend = function ( props ) {

        // extended class with the new prototype
        var NewClass = function () {
            var r;

            // Recall as new if new isn't provided :)
            if( Util.isnt.instanceof( NewClass, this ) ) {
                return ClassUtil.construct( NewClass, arguments );
            }

            // call the constructor
            if ( Util.is.Function( this.initialize ) ) {
                r = this.initialize.apply(this, arguments);
            }

            // call all constructor hooks
            this.callInitHooks();

            return typeof r != 'undefined' ? r : this;
        };

        // Create a prototype
        var proto = ClassUtil.create( this.prototype );
        proto.constructor = NewClass;
        NewClass.prototype = proto;

        // inherit parent's statics
        for (var i in this) {
            if (this.hasOwnProperty(i) && i !== 'prototype') {
                NewClass[i] = this[i];
            }
        }

        // mix static properties into the class
        if ( props.statics ) {
            Util.extend( NewClass, props.statics );
            delete props.statics;
        }

        // Apply over statics the parent Class
        NewClass.parent = this;

        // mix includes into the prototype
        if ( props.includes ) {
            Util.extend.apply( null, [ proto ].concat( props.includes ) );
            delete props.includes;
        }

        // mix given properties into the prototype
        Util.extend( proto, props );

        return NewClass;
    };

    // method for adding properties to prototype
    Class.mixin = Class.include = function (props) {
        Util.extend(this.prototype, props);
    };

};


var fs = require('fs'),
    path = require('path'),
    gutil = require('gulp-util'),
    PluginError = gutil.PluginError,
    through = require('through2'),
    defaults = require('lodash.defaults'),
    parseCss = require('css-parse');

var PLUGIN_NAME = 'gulp-extract-css';

module.exports = function (options) {
  'use strict';

  // Default options
  var options = defaults(options || {}, {
    log: false,
    takeout: []
  });

  // Log info only when 'options.log' is set to true
  var log = function (message) {
    if (options.log) {
      gutil.log(message);
    }
  };

  // Define a counter that counts the rules that are iterated over
  var rulecount = 0;

  // Distribute content to the cssfiles object
  function distribute(filename, content){
    content = content;
    fs.appendFile(filename, content + "\n");
    log(rulecount + ': Append to ' + filename + ' -> ' + content);
  }

  // Check if a rule has selectors
  function hasSelectors(rule){
    return rule.selectors.length > 0;
  }

  // Wrap output inside media query
  function wrapMedia(mq, output){
    return '@media ' + mq + ' { ' + output + ' }';
  }

  // Detect weather a selector should be extracted or not
  function detectTakeout(selectors){
    var properties = {
      takeout: false
    };

    options.takeout.forEach(function (takeout) {
      selectors.forEach(function (selector) {
        if (selector.indexOf(takeout.ruleprefix) === 0) {
          properties.takeout = true;
          properties.filename = takeout.filename;
        }
      });
    });

    return properties;
  }

  function transform(file, enc, cb) {

    if (file.isNull()) {
      this.push(file);
      return cb();
    }

    if (file.isStream()) {
      this.emit('error', new PluginError(PLUGIN_NAME, 'Streaming not supported'));
      return cb();
    }

    // Check if configured to extract styles
    if (options.takeout && options.takeout.length > 0) {

      var filename = path.relative(file.cwd, file.path);
      var source = file.contents.toString('utf8');
      var cssJson = parseCss(source);
      var outputDir = path.relative(file.cwd, path.dirname(file.path));

      // Prepare files
      fs.writeFile(filename, '');
      options.takeout.forEach(function (takeout) {
        fs.writeFile(outputDir + '/' + takeout.filename, '');
      });

      cssJson.stylesheet.rules.forEach(function(rule){

        if (rule.type === "media"){

          rule.rules.forEach(function(subrule) {
            if (typeof subrule.selectors !== 'undefined' && typeof subrule.declarations !== 'undefined'){
              var declarations = '';

              subrule.declarations.forEach(function(declaration){
                if (declaration.type === 'declaration'){
                  declarations += declaration.property + ': ' + declaration.value + ';';
                }
              });

              var styles = subrule.selectors + ' { ' + declarations + ' } ';
              var check = detectTakeout(subrule.selectors);

              if (check.takeout){
                distribute(outputDir + '/' + check.filename, wrapMedia(rule.media, styles));
              } else {
                distribute(filename, wrapMedia(rule.media, styles));
              }

            }
          });

        } else {

          if (typeof rule.selectors !== 'undefined' && typeof rule.declarations !== 'undefined') {

            var declarations = '';

            rule.declarations.forEach(function(declaration){
              if (declaration.type === 'declaration'){
                declarations += declaration.property + ': ' + declaration.value + ';';
              }
            });

            var styles = rule.selectors + ' { ' + declarations + ' } ';
            var check = detectTakeout(rule.selectors);

            if (check.takeout){
              distribute(outputDir + '/' + check.filename, styles);
            } else {
              distribute(filename, styles);
            }

          }

        }

        rulecount++;

      });

    } else {
      log(PLUGIN_NAME + ' is not configured to extract anything');
    }

    cb();
  }

  return through.obj(transform);
};


var domHelpers = require('../helpers-dom.js');

var WORD_SPLIT = /\s+/;
var CONTAINS_WORDS = /(\w)(\s+)(\w)/;

function TitleHeaderItem(algoritme, node) {
  this._algoritme = algoritme;

  this.node = node;
  this._tagname = node.tagname;

  this.text = node.getText().trim();
  this.words = [];

  this._level = 0;
  this._wordscore = 0;
  this._levenshtein = [];
  this._worddiff = [];
  this._fussy = [];

  this.likelihood = 0;

  this.ignore = this.text.length >= 200 || !CONTAINS_WORDS.test(this.text);
  if (this.ignore === false) {
    this.words = this.text.split(WORD_SPLIT);
    this._calculateScores();

    for (var i = 0, l = algoritme._meta.length; i < l; i++) {
      this.appendDistance(algoritme._meta[i]);
    }
  }
}
module.exports = TitleHeaderItem;

// Linear scale function between:
// f(6) = 0.1
// f(1) = 1
function headerScore(x) {
  return -0.18 * x + 1.18;
}

// This function is made by the following requirements:
// x > 5, y = 1
// f(x) = a*sqrt(x) + b*x^2 + c
// f(2) = 0.2
// f(5) = 1
// f'(5) = 0
function wordcountScore(x) {
  if (x > 5) return 1;
  else return Math.min(1, 2.27 * Math.sqrt(x) - 0.0507 * Math.pow(x, 2) - 2.808);
}

// Calculate the none meta comparison scores
TitleHeaderItem.prototype._calculateScores = function () {
  var algoritme = this._algoritme;

  // set level property
  this._level = 0.5;
  if (domHelpers.HEADERS.hasOwnProperty(this._tagname)) {
    this._level = headerScore(Number(this._tagname.slice(1)));
  }

  // set wordscore property
  this._wordscore = wordcountScore(this.words.length);

  // update range objects
  algoritme._adjustLevel.update(this._level);
  algoritme._adjustWordscore.update(this._wordscore);
};

// Calculate meta comparison scores
TitleHeaderItem.prototype.appendDistance = function (meta) {
  this._levenshtein.push(meta.calculateLevenshtein(this));
  this._worddiff.push(meta.calculateWorddiff(this));
  this._fussy.push(meta.calculateFussy(this));
};

  // Set the .likelihood property
TitleHeaderItem.prototype.calculateLikelihood = function () {
  var algoritme = this._algoritme;
  var meta = algoritme._meta;

  var levenshteinTotal = 0,
      worddiffTotal = 0,
      fussyTotal = 0;

  var divider = 0;
  for (var i = 0, l = meta.length; i < l; i++) {
    levenshteinTotal += meta[i]._levenshtein.appearAdjust(this._levenshtein[i], meta[i].appear);
    worddiffTotal += meta[i]._worddiff.appearAdjust(this._worddiff[i], meta[i].appear);
    fussyTotal += meta[i]._fussy.appearAdjust(this._fussy[i], meta[i].appear);

    divider += meta[i].appear;
  }

  // The adjust method expect high values to be (likely) but in this
  // case lower values are actaully the best, so 1 - likelihood transforms
  // it intro 1 (likely), 0 (unlikly)
  var levenshtein = 1 - (levenshteinTotal / divider),
      worddiff = 1 - (worddiffTotal / divider),
      fussy = 1 - (fussyTotal / divider),

  // These where created using mathematical functions there followed the
  // 1 ~ likely rule.
      wordscore = algoritme._adjustWordscore.adjust(this._wordscore),
      level = algoritme._adjustLevel.adjust(this._level);

  // Then calculate the avg. likelihood
  this.likelihood = (levenshtein + worddiff + level + fussy + wordscore) / 5;
};