/*
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org
    
    This file is part of Zotero.
    
    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
    
    ***** END LICENSE BLOCK *****
*/


Zotero.Debug = new function () {
	var _console, _stackTrace, _store, _level, _lastTime, _output = [];
	var _slowTime = false;
	var _colorOutput = false;
	var _consoleViewer = false;
	var _consoleViewerQueue = [];
	var _consoleViewerListener;
	var _listeners = [];
	
	/**
	 * Initialize debug logging
	 *
	 * Debug logging can be set in several different ways:
	 *
	 *   - via the debug.log pref in the client or connector
	 *   - by enabling debug output logging from the Help menu
	 *   - by passing -ZoteroDebug or -ZoteroDebugText on the command line
	 *
	 * In the client, debug.log and -ZoteroDebugText enable logging via the terminal, while -ZoteroDebug
	 * enables logging via an in-app HTML-based window.
	 *
	 * @param {Integer} [forceDebugLog = 0] - Force output even if pref disabled
	 *    2: window (-ZoteroDebug)
	 *    1: text console (-ZoteroDebugText)
	 *    0: disabled
	 */
	this.init = function (forceDebugLog = 0) {
		_console = Zotero.Prefs.get('debug.log') || forceDebugLog == 1;
		_consoleViewer = forceDebugLog == 2;
		// When logging to the text console from the client on Mac/Linux, colorize output
		if (_console && Zotero.isFx && !Zotero.isBookmarklet) {
			_colorOutput = true;
			
			// Time threshold in ms above which intervals should be colored red in terminal output
			_slowTime = Zotero.Prefs.get('debug.log.slowTime');
		}
		_store = Zotero.Prefs.get('debug.store');
		if (_store) {
			Zotero.Prefs.set('debug.store', false);
		}
		_level = Zotero.Prefs.get('debug.level');
		_stackTrace = Zotero.Prefs.get('debug.stackTrace');
		
		this.storing = _store;
		this.updateEnabled();
		
		// Enable dump() from window (non-XPCOM) scopes when terminal or viewer logging is enabled.
		// (These will always go to the terminal, even in viewer mode.)
		Zotero.Prefs.set('browser.dom.window.dump.enabled', _console || _consoleViewer || Zotero.test, true);
		
		if (_consoleViewer) {
			setTimeout(function () {
				Zotero.openInViewer("chrome://zotero/content/debugViewer.html");
			}, 1000);
		}
	}
	
	this.log = function (message, level, maxDepth, stack) {
		if (!this.enabled) {
			return;
		}
		
		if (typeof message != 'string') {
			message = Zotero.Utilities.varDump(message, 0, maxDepth);
		}
		
		if (!level) {
			level = 3;
		}
		
		// If level above debug.level value, don't display
		if (level > _level) {
			return;
		}
		
		var deltaStr = '';
		var deltaStrStore = '';
		var delta = 0;
		var d = new Date();
		if (_lastTime) {
			delta = d - _lastTime;
		}
		_lastTime = d;
		var slowPrefix = "";
		var slowSuffix = "";
		if (_slowTime && delta > _slowTime) {
			slowPrefix = "\x1b[31;40m";
			slowSuffix = "\x1b[0m";
		}
		
		delta = ("" + delta).padStart(7, "0")
		
		deltaStr = "(" + slowPrefix + "+" + delta + slowSuffix + ")";
		if (_store) {
			deltaStrStore = "(+" + delta + ")";
		}
		
		if (stack === true) {
			// Display stack starting from where this was called
			stack = Components.stack.caller;
		} else if (stack >= 0) {
			let i = stack;
			stack = Components.stack.caller;
			while(stack && i--) {
				stack = stack.caller;
			}
		} else if (_stackTrace) {
			// Stack trace enabled globally
			stack = Components.stack.caller;
		} else {
			stack = undefined;
		}
		
		if (stack) {
			message += '\n' + this.stackToString(stack);
		}
		
		if (_console || _consoleViewer || _listeners.length) {
			var output = '(' + level + ')' + deltaStr + ': ' + message;
			if (Zotero.isFx && !Zotero.isBookmarklet) {
				// Text console
				if (_console) {
					dump("zotero" + output + "\n\n");
				}
				
				// Remove ANSI color codes for the viewer and listeners. We could replace this with
				// HTML for the viewer, but it's probably unnecessarily distracting/alarming to show
				// the red. Devs who care about times should just use a terminal.
				if (slowPrefix) {
					output = output.replace(slowPrefix, '').replace(slowSuffix, '');
				}
				
				// Console window
				if (_consoleViewer) {
					// If there's a listener, pass line immediately
					if (_consoleViewerListener) {
						_consoleViewerListener(output);
					}
					// Otherwise add to queue
					else {
						_consoleViewerQueue.push(output);
					}
				}
				
				// Other listeners
				if (_listeners.length) {
					for (let listener of _listeners) {
						listener(output);
					}
				}
			} else if(window.console) {
				window.console.log(output);
			}
		}
		if (_store) {
			if (Math.random() < 1/1000) {
				// Remove initial lines if over limit
				var overage = this.count() - Zotero.Prefs.get('debug.store.limit');
				if (overage > 0) {
					_output.splice(0, Math.abs(overage));
				}
			}
			_output.push('(' + level + ')' + deltaStrStore + ': ' + message);
		}
	}
	
	
	this.get = function (maxChars, maxLineLength) {
		var output = _output;
		var total = output.length;
		
		if (total == 0) {
			return "";
		}
		
		if (maxLineLength) {
			for (var i=0, len=output.length; i<len; i++) {
				if (output[i].length > maxLineLength) {
					output[i] = Zotero.Utilities.ellipsize(output[i], maxLineLength, false, true);
				}
			}
		}
		
		output = output.join('\n\n');
		
		if (maxChars) {
			output = output.substr(maxChars * -1);
			// Cut at two newlines
			let matches = output.match(/^[\n]*\n\n/);
			if (matches) {
				output = output.substr(matches[0].length);
			}
		}

		return Zotero.getSystemInfo().then(function (sysInfo) {
			return Zotero.getErrors(true).join('\n\n') +
				"\n\n" + sysInfo + "\n\n" +
				"=========================================================\n\n" +
				output;
		});
	};
	
	
	this.addListener = function (listener) {
		this.enabled = true;
		_listeners.push(listener);
	};
	
	
	this.removeListener = function (listener) {
		var pos = _listeners.indexOf(listener);
		if (pos != -1) {
			_listeners.splice(pos, 1);
		}
		this.updateEnabled();
	};
	
	
	this.getConsoleViewerOutput = function () {
		var queue = _output.concat(_consoleViewerQueue);
		_consoleViewerQueue = [];
		return queue;
	}
	
	
	this.addConsoleViewerListener = function (listener) {
		this.enabled = _consoleViewer = true;
		_consoleViewerListener = listener;
	};
	
	
	this.removeConsoleViewerListener = function () {
		_consoleViewerListener = null;
		// At least for now, stop logging once console viewer is closed
		_consoleViewer = false;
		this.updateEnabled();
	};
	
	
	this.setStore = function (enable) {
		if (enable) {
			this.clear();
		}
		_store = enable;
		this.updateEnabled();
		this.storing = _store;
	}
	
	
	this.updateEnabled = function () {
		this.enabled = _console || _consoleViewer || _store || _listeners.length;
	};
	
	
	this.count = function () {
		return _output.length;
	}
	
	
	this.clear = function () {
		_output = [];
	}
	
	/**
	 * Format a stack trace for output in the same way that Error.stack does
	 * @param {Components.stack} stack
	 * @param {Integer} [lines=5] Number of lines to format
	 */
	this.stackToString = function (stack, lines) {
		if (!lines) lines = 5;
		var str = '';
		while (stack && lines--) {
			str += '\n  ' + (stack.name || '') + '@' + stack.filename
				+ ':' + stack.lineNumber;
			stack = stack.caller;
		}
		return this.filterStack(str).substring(1);
	};
	
	
	/**
	 * This method previously filtered Bluebird stack frames out of a stack
	 * trace. Bluebird has been removed, so this function now only exists for
	 * compatibility (and in case filtering is necessary in the future).
	 *
	 * @param {string} stack An error stack trace string
	 * @returns {string} The same stack
	 */
	this.filterStack = function (stack) {
		return stack;
	};
};
