var Stream = require('stream');
var json = typeof JSON === 'object' ? JSON : require('jsonify');
var through = require('through');
var nextTick = typeof setImmediate !== 'undefined'
    ? setImmediate
    : process.nextTick
;

module.exports = function () {
    var output = through();
    output.pause();
    output.queue('TAP version 13\n');
    
    var results = new Results(output);
    output.push = function (t) { results.push(t) };
    
    output.only = function (name) {
        results.only = name;
    };
    
    nextTick(function next () {
        var t = results.tests.shift();
        if (!t && results.running) return;
        if (!t) return results.close();
        t.run();
    });
    
    return output;
};

function Results (stream) {
    this.count = 0;
    this.fail = 0;
    this.pass = 0;
    this.stream = stream;
    this.tests = [];
    this.running = 0;
}

Results.prototype.push = function (t, parentT) {
    var self = this;
    var write = function (s) { self.stream.queue(s) };
    t.once('prerun', function () {
        if (self.only && self.only !== t.name && !parentT) {
            var nt = self.tests.shift();
            if (nt) nt.run()
            else self.close();
            return;
        }
        
        self.running ++;
        write('# ' + t.name + '\n');
    });
    if (parentT) {
        var ix = self.tests.indexOf(parentT);
        if (ix >= 0) self.tests.splice(ix, 0, t);
    }
    else self.tests.push(t);
    
    var plan;
    t.on('plan', function (n) { plan = n });
    
    var subtests = 0;
    
    t.on('test', function (st) {
        subtests ++;
        st.on('end', function () {
            subtests --;
            if (subtests === 1) nextTick(function () { st.run() });
            else if (subtests === 0 && !t.ended) {
                t.end();
            }
        });
        self.push(st, t);
        if (subtests === 1) {
            if (plan === undefined) st.run();
            else nextTick(function () {
                st.run();
            });
        }
    });
    
    t.on('result', function (res) {
        if (typeof res === 'string') {
            write('# ' + res + '\n');
            return;
        }
        write(encodeResult(res, self.count + 1));
        self.count ++;
        
        if (res.ok) self.pass ++
        else self.fail ++
    });
    
    t.once('end', function () {
        if (t._skip) {
            var nt = self.tests.shift();
            if (nt) nt.run();
            else self.close();
            return;
        }
        
        self.running --;
        if (subtests !== 0) return;
        
        if (self.running === 0 && self.tests.length) {
            var nt = self.tests.shift();
            nt.run();
        }
        else if (self.running === 0) {
            self.close();
        }
    });
};

Results.prototype.close = function () {
    var self = this;
    if (self.closed) self.stream.emit('error', new Error('ALREADY CLOSED'));
    self.closed = true;
    var write = function (s) { self.stream.queue(s) };
    
    write('\n1..' + self.count + '\n');
    write('# tests ' + self.count + '\n');
    write('# pass  ' + self.pass + '\n');
    if (self.fail) write('# fail  ' + self.fail + '\n')
    else write('\n# ok\n')
    
    self.stream.queue(null);
};

function encodeResult (res, count) {
    var output = '';
    output += (res.ok ? 'ok ' : 'not ok ') + count;
    output += res.name ? ' ' + res.name.replace(/\s+/g, ' ') : '';
    
    if (res.skip) output += ' # SKIP';
    else if (res.todo) output += ' # TODO';
    
    output += '\n';
    if (res.ok) return output;
    
    var outer = '  ';
    var inner = outer + '  ';
    output += outer + '---\n';
    output += inner + 'operator: ' + res.operator + '\n';
    
    var ex = json.stringify(res.expected, getSerialize()) || '';
    var ac = json.stringify(res.actual, getSerialize()) || '';
    
    if (Math.max(ex.length, ac.length) > 65) {
        output += inner + 'expected:\n' + inner + '  ' + ex + '\n';
        output += inner + 'actual:\n' + inner + '  ' + ac + '\n';
    }
    else {
        output += inner + 'expected: ' + ex + '\n';
        output += inner + 'actual:   ' + ac + '\n';
    }
    if (res.at) {
        output += inner + 'at: ' + res.at + '\n';
    }
    if (res.operator === 'error' && res.actual && res.actual.stack) {
        var lines = String(res.actual.stack).split('\n');
        output += inner + 'stack:\n';
        output += inner + '  ' + lines[0] + '\n';
        for (var i = 1; i < lines.length; i++) {
            output += inner + lines[i] + '\n';
        }
    }
    
    output += outer + '...\n';
    return output;
}

function getSerialize () {
    var seen = [];
    
    return function (key, value) {
        var ret = value;
        if (typeof value === 'object' && value) {
            var found = false;
            for (var i = 0; i < seen.length; i++) {
                if (seen[i] === value) {
                    found = true
                    break;
                }
            }
            
            if (found) ret = '[Circular]'
            else seen.push(value)
        }
        return ret;
    };
}
