'use strict';
var StringDecoder = require('string_decoder').StringDecoder;
var cliCursor = require('cli-cursor');
var lastLineTracker = require('last-line-stream/tracker');
var plur = require('plur');
var spinners = require('cli-spinners');
var chalk = require('chalk');
var cliTruncate = require('cli-truncate');
var cross = require('figures').cross;
var repeating = require('repeating');
var objectAssign = require('object-assign');
var colors = require('../colors');

chalk.enabled = true;
Object.keys(colors).forEach(function (key) {
	colors[key].enabled = true;
});

function MiniReporter(options) {
	if (!(this instanceof MiniReporter)) {
		return new MiniReporter(options);
	}

	var spinnerDef = spinners[process.platform === 'win32' ? 'line' : 'dots'];
	this.spinnerFrames = spinnerDef.frames.map(function (c) {
		return chalk.gray.dim(c);
	});
	this.spinnerInterval = spinnerDef.interval;

	this.options = objectAssign({}, options);

	this.reset();
	this.stream = process.stderr;
	this.stringDecoder = new StringDecoder();
}

module.exports = MiniReporter;

MiniReporter.prototype.start = function () {
	var self = this;

	this.interval = setInterval(function () {
		self.spinnerIndex = (self.spinnerIndex + 1) % self.spinnerFrames.length;
		self.write(self.prefix());
	}, this.spinnerInterval);

	return this.prefix('');
};

MiniReporter.prototype.reset = function () {
	this.clearInterval();
	this.passCount = 0;
	this.knownFailureCount = 0;
	this.failCount = 0;
	this.skipCount = 0;
	this.todoCount = 0;
	this.rejectionCount = 0;
	this.exceptionCount = 0;
	this.currentStatus = '';
	this.currentTest = '';
	this.statusLineCount = 0;
	this.spinnerIndex = 0;
	this.lastLineTracker = lastLineTracker();
};

MiniReporter.prototype.spinnerChar = function () {
	return this.spinnerFrames[this.spinnerIndex];
};

MiniReporter.prototype.clearInterval = function () {
	clearInterval(this.interval);
	this.interval = null;
};

MiniReporter.prototype.test = function (test) {
	if (test.todo) {
		this.todoCount++;
	} else if (test.skip) {
		this.skipCount++;
	} else if (test.error) {
		this.failCount++;
	} else {
		this.passCount++;
		if (test.failing) {
			this.knownFailureCount++;
		}
	}

	if (test.todo || test.skip) {
		return;
	}

	return this.prefix(this._test(test));
};

MiniReporter.prototype.prefix = function (str) {
	str = str || this.currentTest;
	this.currentTest = str;

	// The space before the newline is required for proper formatting. (Not sure why).
	return ' \n ' + this.spinnerChar() + ' ' + str;
};

MiniReporter.prototype._test = function (test) {
	var SPINNER_WIDTH = 3;
	var PADDING = 1;
	var title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING);

	if (test.error || test.failing) {
		title = colors.error(test.title);
	}

	return title + '\n' + this.reportCounts();
};

MiniReporter.prototype.unhandledError = function (err) {
	if (err.type === 'exception') {
		this.exceptionCount++;
	} else {
		this.rejectionCount++;
	}
};

MiniReporter.prototype.reportCounts = function (time) {
	var lines = [
		this.passCount > 0 ? '\n   ' + colors.pass(this.passCount, 'passed') : '',
		this.knownFailureCount > 0 ? '\n   ' + colors.error(this.knownFailureCount, plur('known failure', this.knownFailureCount)) : '',
		this.failCount > 0 ? '\n   ' + colors.error(this.failCount, 'failed') : '',
		this.skipCount > 0 ? '\n   ' + colors.skip(this.skipCount, 'skipped') : '',
		this.todoCount > 0 ? '\n   ' + colors.todo(this.todoCount, 'todo') : ''
	].filter(Boolean);

	if (time && lines.length > 0) {
		lines[0] += ' ' + time;
	}

	return lines.join('');
};

MiniReporter.prototype.finish = function (runStatus) {
	this.clearInterval();
	var time;

	if (this.options.watching) {
		time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
	}

	var status = this.reportCounts(time);

	if (this.rejectionCount > 0) {
		status += '\n   ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
	}

	if (this.exceptionCount > 0) {
		status += '\n   ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount));
	}

	if (runStatus.previousFailCount > 0) {
		status += '\n   ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun');
	}

	var i = 0;

	if (this.knownFailureCount > 0) {
		runStatus.knownFailures.forEach(function (test) {
			i++;

			var title = test.title;

			status += '\n\n\n   ' + colors.error(i + '.', title);
			// TODO output description with link
			// status += colors.stack(description);
		});
	}

	if (this.failCount > 0) {
		runStatus.errors.forEach(function (test) {
			if (!test.error || !test.error.message) {
				return;
			}

			i++;

			var title = test.error ? test.title : 'Unhandled Error';
			var description;

			if (test.error) {
				description = '   ' + test.error.message + '\n  ' + stripFirstLine(test.error.stack).trimRight();
			} else {
				description = JSON.stringify(test);
			}

			status += '\n\n\n   ' + colors.error(i + '.', title) + '\n';
			status += colors.stack(description);
		});
	}

	if (this.rejectionCount > 0 || this.exceptionCount > 0) {
		runStatus.errors.forEach(function (err) {
			if (err.title) {
				return;
			}

			i++;

			if (err.type === 'exception' && err.name === 'AvaError') {
				status += '\n\n\n   ' + colors.error(cross + ' ' + err.message);
			} else {
				var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
				var description = err.stack ? err.stack.trimRight() : JSON.stringify(err);

				status += '\n\n\n   ' + colors.error(i + '.', title) + '\n';
				status += '   ' + colors.stack(description);
			}
		});
	}

	return status + '\n';
};

MiniReporter.prototype.section = function () {
	return '\n' + chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80));
};

MiniReporter.prototype.clear = function () {
	return '';
};

MiniReporter.prototype.write = function (str) {
	cliCursor.hide();
	this.currentStatus = str;
	this._update();
	this.statusLineCount = this.currentStatus.split('\n').length;
};

MiniReporter.prototype.stdout = MiniReporter.prototype.stderr = function (data) {
	this._update(data);
};

MiniReporter.prototype._update = function (data) {
	var str = '';
	var ct = this.statusLineCount;
	var columns = process.stdout.columns;
	var lastLine = this.lastLineTracker.lastLine();

	// Terminals automatically wrap text. We only need the last log line as seen on the screen.
	lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns));

	// Don't delete the last log line if it's completely empty.
	if (lastLine.length) {
		ct++;
	}

	// Erase the existing status message, plus the last log line.
	str += eraseLines(ct);

	// Rewrite the last log line.
	str += lastLine;

	if (str.length) {
		this.stream.write(str);
	}

	if (data) {
		// send new log data to the terminal, and update the last line status.
		this.lastLineTracker.update(this.stringDecoder.write(data));
		this.stream.write(data);
	}

	var currentStatus = this.currentStatus;

	if (currentStatus.length) {
		lastLine = this.lastLineTracker.lastLine();
		// We need a newline at the end of the last log line, before the status message.
		// However, if the last log line is the exact width of the terminal a newline is implied,
		// and adding a second will cause problems.
		if (lastLine.length % columns) {
			currentStatus = '\n' + currentStatus;
		}
		// rewrite the status message.
		this.stream.write(currentStatus);
	}
};

// TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution.
var CSI = '\u001b[';
var ERASE_LINE = CSI + '2K';
var CURSOR_TO_COLUMN_0 = CSI + '0G';
var CURSOR_UP = CSI + '1A';

// Returns a string that will erase `count` lines from the end of the terminal.
function eraseLines(count) {
	var clear = '';

	for (var i = 0; i < count; i++) {
		clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : '');
	}

	if (count) {
		clear += CURSOR_TO_COLUMN_0;
	}

	return clear;
}

function stripFirstLine(message) {
	return message.replace(/^[^\n]*\n/, '');
}