// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Definition of the DebugWindow class. Please minimize
 * dependencies this file has on other closure classes as any dependency it
 * takes won't be able to use the logging infrastructure.
 *
 */

goog.provide('goog.debug.DebugWindow');

goog.require('goog.debug.HtmlFormatter');
goog.require('goog.debug.LogManager');
goog.require('goog.debug.Logger');
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeStyleSheet');
goog.require('goog.string.Const');
goog.require('goog.structs.CircularBuffer');
goog.require('goog.userAgent');



/**
 * Provides a debug DebugWindow that is bound to the goog.debug.Logger.
 * It handles log messages and writes them to the DebugWindow. This doesn't
 * provide a lot of functionality that the old Gmail logging infrastructure
 * provided like saving debug logs for exporting to the server. Now that we
 * have an event-based logging infrastructure, we can encapsulate that
 * functionality in a separate class.
 *
 * @constructor
 * @param {string=} opt_identifier Identifier for this logging class.
 * @param {string=} opt_prefix Prefix prepended to messages.
 */
goog.debug.DebugWindow = function(opt_identifier, opt_prefix) {
  /**
   * Identifier for this logging class
   * @protected {string}
   */
  this.identifier = opt_identifier || '';

  /**
   * Array used to buffer log output
   * @protected {!Array<!goog.html.SafeHtml>}
   */
  this.outputBuffer = [];

  /**
   * Optional prefix to be prepended to error strings
   * @private {string}
   */
  this.prefix_ = opt_prefix || '';

  /**
   * Buffer for saving the last 1000 messages
   * @private {!goog.structs.CircularBuffer}
   */
  this.savedMessages_ =
      new goog.structs.CircularBuffer(goog.debug.DebugWindow.MAX_SAVED);

  /**
   * Save the publish handler so it can be removed
   * @private {!Function}
   */
  this.publishHandler_ = goog.bind(this.addLogRecord, this);

  /**
   * Formatter for formatted output
   * @private {goog.debug.Formatter}
   */
  this.formatter_ = new goog.debug.HtmlFormatter(this.prefix_);

  /**
   * Loggers that we shouldn't output
   * @private {!Object}
   */
  this.filteredLoggers_ = {};

  // enable by default
  this.setCapturing(true);

  /**
   * Whether we are currently enabled. When the DebugWindow is enabled, it tries
   * to keep its window open. When it's disabled, it can still be capturing log
   * output if, but it won't try to write them to the DebugWindow window until
   * it's enabled.
   * @private {boolean}
   */
  this.enabled_ = goog.debug.DebugWindow.isEnabled(this.identifier);

  // timer to save the DebugWindow's window position in a cookie
  goog.global.setInterval(goog.bind(this.saveWindowPositionSize_, this), 7500);
};


/**
 * Max number of messages to be saved
 * @type {number}
 */
goog.debug.DebugWindow.MAX_SAVED = 500;


/**
 * How long to keep the cookies for in milliseconds
 * @type {number}
 */
goog.debug.DebugWindow.COOKIE_TIME = 30 * 24 * 60 * 60 * 1000; // 30-days


/**
 * HTML string printed when the debug window opens
 * @type {string}
 * @protected
 */
goog.debug.DebugWindow.prototype.welcomeMessage = 'LOGGING';


/**
 * Whether to force enable the window on a severe log.
 * @type {boolean}
 * @private
 */
goog.debug.DebugWindow.prototype.enableOnSevere_ = false;


/**
 * Reference to debug window
 * @type {Window}
 * @protected
 */
goog.debug.DebugWindow.prototype.win = null;


/**
 * In the process of opening the window
 * @type {boolean}
 * @private
 */
goog.debug.DebugWindow.prototype.winOpening_ = false;


/**
 * Whether we are currently capturing logger output.
 *
 * @type {boolean}
 * @private
 */
goog.debug.DebugWindow.prototype.isCapturing_ = false;


/**
 * Whether we already showed an alert that the DebugWindow was blocked.
 * @type {boolean}
 * @private
 */
goog.debug.DebugWindow.showedBlockedAlert_ = false;


/**
 * Reference to timeout used to buffer the output stream.
 * @type {?number}
 * @private
 */
goog.debug.DebugWindow.prototype.bufferTimeout_ = null;


/**
 * Timestamp for the last time the log was written to.
 * @protected {number}
 */
goog.debug.DebugWindow.prototype.lastCall = goog.now();


/**
 * Sets the welcome message shown when the window is first opened or reset.
 *
 * @param {string} msg An HTML string.
 */
goog.debug.DebugWindow.prototype.setWelcomeMessage = function(msg) {
  this.welcomeMessage = msg;
};


/**
 * Initializes the debug window.
 */
goog.debug.DebugWindow.prototype.init = function() {
  if (this.enabled_) {
    this.openWindow_();
  }
};


/**
 * Whether the DebugWindow is enabled. When the DebugWindow is enabled, it
 * tries to keep its window open and logs all messages to the window.  When the
 * DebugWindow is disabled, it stops logging messages to its window.
 *
 * @return {boolean} Whether the DebugWindow is enabled.
 */
goog.debug.DebugWindow.prototype.isEnabled = function() {
  return this.enabled_;
};


/**
 * Sets whether the DebugWindow is enabled. When the DebugWindow is enabled, it
 * tries to keep its window open and log all messages to the window. When the
 * DebugWindow is disabled, it stops logging messages to its window. The
 * DebugWindow also saves this state to a cookie so that it's persisted across
 * application refreshes.
 * @param {boolean} enable Whether the DebugWindow is enabled.
 */
goog.debug.DebugWindow.prototype.setEnabled = function(enable) {
  this.enabled_ = enable;

  if (this.enabled_) {
    this.openWindow_();
  }

  this.setCookie_('enabled', enable ? '1' : '0');
};


/**
 * Sets whether the debug window should be force enabled when a severe log is
 * encountered.
 * @param {boolean} enableOnSevere Whether to enable on severe logs..
 */
goog.debug.DebugWindow.prototype.setForceEnableOnSevere =
    function(enableOnSevere) {
  this.enableOnSevere_ = enableOnSevere;
};


/**
 * Whether we are currently capturing logger output.
 * @return {boolean} whether we are currently capturing logger output.
 */
goog.debug.DebugWindow.prototype.isCapturing = function() {
  return this.isCapturing_;
};


/**
 * Sets whether we are currently capturing logger output.
 * @param {boolean} capturing Whether to capture logger output.
 */
goog.debug.DebugWindow.prototype.setCapturing = function(capturing) {
  if (capturing == this.isCapturing_) {
    return;
  }
  this.isCapturing_ = capturing;

  // attach or detach handler from the root logger
  var rootLogger = goog.debug.LogManager.getRoot();
  if (capturing) {
    rootLogger.addHandler(this.publishHandler_);
  } else {
    rootLogger.removeHandler(this.publishHandler_);
  }
};


/**
 * Gets the formatter for outputting to the debug window. The default formatter
 * is an instance of goog.debug.HtmlFormatter
 * @return {goog.debug.Formatter} The formatter in use.
 */
goog.debug.DebugWindow.prototype.getFormatter = function() {
  return this.formatter_;
};


/**
 * Sets the formatter for outputting to the debug window.
 * @param {goog.debug.Formatter} formatter The formatter to use.
 */
goog.debug.DebugWindow.prototype.setFormatter = function(formatter) {
  this.formatter_ = formatter;
};


/**
 * Adds a separator to the debug window.
 */
goog.debug.DebugWindow.prototype.addSeparator = function() {
  this.write_(goog.html.SafeHtml.create('hr'));
};


/**
 * @return {boolean} Whether there is an active window.
 */
goog.debug.DebugWindow.prototype.hasActiveWindow = function() {
  return !!this.win && !this.win.closed;
};


/**
 * Clears the contents of the debug window
 * @protected
 */
goog.debug.DebugWindow.prototype.clear = function() {
  this.savedMessages_.clear();
  if (this.hasActiveWindow()) {
    this.writeInitialDocument();
  }
};


/**
 * Adds a log record.
 * @param {goog.debug.LogRecord} logRecord the LogRecord.
 */
goog.debug.DebugWindow.prototype.addLogRecord = function(logRecord) {
  if (this.filteredLoggers_[logRecord.getLoggerName()]) {
    return;
  }
  var html = this.formatter_.formatRecordAsHtml(logRecord);
  this.write_(html);
  if (this.enableOnSevere_ &&
      logRecord.getLevel().value >= goog.debug.Logger.Level.SEVERE.value) {
    this.setEnabled(true);
  }
};


/**
 * Writes a message to the log, possibly opening up the window if it's enabled,
 * or saving it if it's disabled.
 * @param {!goog.html.SafeHtml} html The HTML to write.
 * @private
 */
goog.debug.DebugWindow.prototype.write_ = function(html) {
  // If the logger is enabled, open window and write html message to log
  // otherwise save it
  if (this.enabled_) {
    this.openWindow_();
    this.savedMessages_.add(html);
    this.writeToLog_(html);
  } else {
    this.savedMessages_.add(html);
  }
};


/**
 * Write to the buffer.  If a message hasn't been sent for more than 750ms just
 * write, otherwise delay for a minimum of 250ms.
 * @param {!goog.html.SafeHtml} html HTML to post to the log.
 * @private
 */
goog.debug.DebugWindow.prototype.writeToLog_ = function(html) {
  this.outputBuffer.push(html);
  goog.global.clearTimeout(this.bufferTimeout_);

  if (goog.now() - this.lastCall > 750) {
    this.writeBufferToLog();
  } else {
    this.bufferTimeout_ =
        goog.global.setTimeout(goog.bind(this.writeBufferToLog, this), 250);
  }
};


/**
 * Write to the log and maybe scroll into view.
 * @protected
 */
goog.debug.DebugWindow.prototype.writeBufferToLog = function() {
  this.lastCall = goog.now();
  if (this.hasActiveWindow()) {
    var body = this.win.document.body;
    var scroll = body &&
        body.scrollHeight - (body.scrollTop + body.clientHeight) <= 100;

    goog.dom.safe.documentWrite(
        this.win.document, goog.html.SafeHtml.concat(this.outputBuffer));
    this.outputBuffer.length = 0;

    if (scroll) {
      this.win.scrollTo(0, 1000000);
    }
  }
};


/**
 * Writes all saved messages to the DebugWindow.
 * @protected
 */
goog.debug.DebugWindow.prototype.writeSavedMessages = function() {
  var messages = this.savedMessages_.getValues();
  for (var i = 0; i < messages.length; i++) {
    this.writeToLog_(messages[i]);
  }
};


/**
 * Opens the debug window if it is not already referenced
 * @private
 */
goog.debug.DebugWindow.prototype.openWindow_ = function() {
  if (this.hasActiveWindow() || this.winOpening_) {
    return;
  }

  var winpos = this.getCookie_('dbg', '0,0,800,500').split(',');
  var x = Number(winpos[0]);
  var y = Number(winpos[1]);
  var w = Number(winpos[2]);
  var h = Number(winpos[3]);

  this.winOpening_ = true;
  this.win = window.open('', this.getWindowName_(), 'width=' + w +
                         ',height=' + h + ',toolbar=no,resizable=yes,' +
                         'scrollbars=yes,left=' + x + ',top=' + y +
                         ',status=no,screenx=' + x + ',screeny=' + y);

  if (!this.win) {
    if (!this.showedBlockedAlert_) {
      // only show this once
      alert('Logger popup was blocked');
      this.showedBlockedAlert_ = true;
    }
  }

  this.winOpening_ = false;

  if (this.win) {
    this.writeInitialDocument();
  }
};


/**
 * Gets a valid window name for the debug window. Replaces invalid characters in
 * IE.
 * @return {string} Valid window name.
 * @private
 */
goog.debug.DebugWindow.prototype.getWindowName_ = function() {
  return goog.userAgent.IE ?
      this.identifier.replace(/[\s\-\.\,]/g, '_') : this.identifier;
};


/**
 * @return {!goog.html.SafeStyleSheet} The stylesheet, for inclusion in the
 *     initial HTML.
 */
goog.debug.DebugWindow.prototype.getStyleRules = function() {
  return goog.html.SafeStyleSheet.fromConstant(goog.string.Const.from(
      '*{font:normal 14px monospace;}' +
      '.dbg-sev{color:#F00}' +
      '.dbg-w{color:#E92}' +
      '.dbg-sh{background-color:#fd4;font-weight:bold;color:#000}' +
      '.dbg-i{color:#666}' +
      '.dbg-f{color:#999}' +
      '.dbg-ev{color:#0A0}' +
      '.dbg-m{color:#990}'));
};


/**
 * Writes the initial HTML of the debug window.
 * @protected
 */
goog.debug.DebugWindow.prototype.writeInitialDocument = function() {
  if (!this.hasActiveWindow()) {
    return;
  }

  this.win.document.open();

  var div = goog.html.SafeHtml.create(
      'div', {
        'class': 'dbg-ev',
        'style': goog.string.Const.from('text-align:center;')},
      goog.html.SafeHtml.concat(
          this.welcomeMessage,
          goog.html.SafeHtml.create('br'),
          goog.html.SafeHtml.create(
              'small', {}, 'Logger: ' + this.identifier)));
  var html = goog.html.SafeHtml.concat(
      goog.html.SafeHtml.createStyle(this.getStyleRules()),
      goog.html.SafeHtml.create('hr'),
      div,
      goog.html.SafeHtml.create('hr'));

  this.writeToLog_(html);
  this.writeSavedMessages();
};


/**
 * Save persistent data (using cookies) for 1 month (cookie specific to this
 * logger object).
 * @param {string} key Data name.
 * @param {string} value Data value.
 * @private
 */
goog.debug.DebugWindow.prototype.setCookie_ = function(key, value) {
  var fullKey = goog.debug.DebugWindow.getCookieKey_(this.identifier, key);
  document.cookie = fullKey + '=' + encodeURIComponent(value) +
      ';path=/;expires=' +
      (new Date(goog.now() + goog.debug.DebugWindow.COOKIE_TIME)).toUTCString();
};


/**
 * Retrieve data (using cookies).
 * @param {string} key Data name.
 * @param {string=} opt_default Optional default value if cookie doesn't exist.
 * @return {string} Cookie value.
 * @private
 */
goog.debug.DebugWindow.prototype.getCookie_ = function(key, opt_default) {
  return goog.debug.DebugWindow.getCookieValue_(
      this.identifier, key, opt_default);
};


/**
 * Creates a valid cookie key name which is scoped to the given identifier.
 * Substitutes all occurences of invalid cookie name characters (whitespace,
 * ';', and '=') with '_', which is a valid and readable alternative.
 * @see goog.net.Cookies#isValidName
 * @see <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>
 * @param {string} identifier Identifier for logging class.
 * @param {string} key Data name.
 * @return {string} Cookie key name.
 * @private
 */
goog.debug.DebugWindow.getCookieKey_ = function(identifier, key) {
  var fullKey = key + identifier;
  return fullKey.replace(/[;=\s]/g, '_');
};


/**
 * Retrieve data (using cookies).
 * @param {string} identifier Identifier for logging class.
 * @param {string} key Data name.
 * @param {string=} opt_default Optional default value if cookie doesn't exist.
 * @return {string} Cookie value.
 * @private
 */
goog.debug.DebugWindow.getCookieValue_ = function(
    identifier, key, opt_default) {
  var fullKey = goog.debug.DebugWindow.getCookieKey_(identifier, key);
  var cookie = String(document.cookie);
  var start = cookie.indexOf(fullKey + '=');
  if (start != -1) {
    var end = cookie.indexOf(';', start);
    return decodeURIComponent(cookie.substring(start + fullKey.length + 1,
        end == -1 ? cookie.length : end));
  } else {
    return opt_default || '';
  }
};


/**
 * @param {string} identifier Identifier for logging class.
 * @return {boolean} Whether the DebugWindow is enabled.
 */
goog.debug.DebugWindow.isEnabled = function(identifier) {
  return goog.debug.DebugWindow.getCookieValue_(identifier, 'enabled') == '1';
};


/**
 * Saves the window position size to a cookie
 * @private
 */
goog.debug.DebugWindow.prototype.saveWindowPositionSize_ = function() {
  if (!this.hasActiveWindow()) {
    return;
  }
  var x = this.win.screenX || this.win.screenLeft || 0;
  var y = this.win.screenY || this.win.screenTop || 0;
  var w = this.win.outerWidth || 800;
  var h = this.win.outerHeight || 500;
  this.setCookie_('dbg', x + ',' + y + ',' + w + ',' + h);
};


/**
 * Adds a logger name to be filtered.
 * @param {string} loggerName the logger name to add.
 */
goog.debug.DebugWindow.prototype.addFilter = function(loggerName) {
  this.filteredLoggers_[loggerName] = 1;
};


/**
 * Removes a logger name to be filtered.
 * @param {string} loggerName the logger name to remove.
 */
goog.debug.DebugWindow.prototype.removeFilter = function(loggerName) {
  delete this.filteredLoggers_[loggerName];
};


/**
 * Modify the size of the circular buffer. Allows the log to retain more
 * information while the window is closed.
 * @param {number} size New size of the circular buffer.
 */
goog.debug.DebugWindow.prototype.resetBufferWithNewSize = function(size) {
  if (size > 0 && size < 50000) {
    this.clear();
    this.savedMessages_ = new goog.structs.CircularBuffer(size);
  }
};
