// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Shell objects and global helper functions for Chrome 
 * automation shell / debugger.  This file is loaded into the global namespace
 * of the interactive shell, so users can simply call global functions 
 * directly.
 */

// TODO(erikkay): look into how this can be split up into multiple files
// It's currently loaded explicitly by Chrome, so maybe I need an "include"
// or "source" builtin to allow a core source file to reference multiple
// sub-files.

/**
 * Sequence number of the DebugCommand.
 */
DebugCommand.next_seq_ = 0;

/**
 * Command messages to be sent to the debugger.
 * @constructor
 */
function DebugCommand(str) {
  this.command = undefined;
  // first, strip off of the leading word as the command
  var argv = str.split(' ');
  this.user_command = argv.shift();
  // the rest of the string is argv to the command
  str = argv.join(' ');
  if (DebugCommand.aliases[this.user_command])
    this.user_command = DebugCommand.aliases[this.user_command];
 if (this.parseArgs_(str) == 1)
   this.type = "request";	
 if (this.command == undefined)	
   this.command = this.user_command;
};

// Mapping of some control characters to avoid the \uXXXX syntax for most
// commonly used control cahracters.
const ctrlCharMap_ = {
  '\b': '\\b',
  '\t': '\\t',
  '\n': '\\n',
  '\f': '\\f',
  '\r': '\\r',
  '"' : '\\"',
  '\\': '\\\\'
};

// Regular expression matching ", \ and control characters (0x00 - 0x1F)
// globally.
const ctrlCharMatch_ = /["\\\\\x00-\x1F]/g;

/**
 * Convert a String to its JSON representation.
 * @param {String} value - String to be converted
 * @return {String} JSON formatted String
 */
DebugCommand.stringToJSON = function(value) {
  // Check for" , \ and control characters (0x00 - 0x1F).
  if (ctrlCharMatch_.test(value)) {
    // Replace ", \ and control characters (0x00 - 0x1F).
    return '"' + value.replace(ctrlCharMatch_, function (char) {
        // Use charmap if possible.
        var mapped = ctrlCharMap_[char];
        if (mapped) return mapped;
        mapped = char.charCodeAt();
        // Convert control character to unicode escape sequence.
        var dig1 = (Math.floor(mapped / 16));
        var dig2 = (mapped % 16)
        return '\\u00' + dig1.toString(16) + dig2.toString(16);
      })
    + '"';
  }

  // Simple string with no special characters.
  return '"' + value + '"';
};

/**
 * @return {bool} True if x is an integer.
 */
DebugCommand.isInt = function(x) { 
   var y = parseInt(x); 
   if (isNaN(y))
     return false; 
   return x == y && x.toString() == y.toString(); 
};

/**
 * @return {float} log base 10 of num
 */
DebugCommand.log10 = function(num) {
  return Math.log(num)/Math.log(10);
};

/**
 * Take an object and encode it (non-recursively) as a JSON dict.
 * @param {Object} obj - object to encode
 */
DebugCommand.toJSON = function(obj) {
  // TODO(erikkay): use a real JSON library
  var json = '{';
  for (var key in obj) {
    if (json.length > 1)
      json += ",";
    var val = obj[key];
    if (!DebugCommand.isInt(val)) {
      val = DebugCommand.stringToJSON(val.toString());
    }
    json += '"' + key + '":' + val;
  }
  json += '}';
  return json;
};

/**
 * Encode the DebugCommand object into the V8 debugger JSON protocol format.
 */
DebugCommand.prototype.toJSONProtocol = function() {
  // TODO(erikkay): use a real JSON library
  var json = '{';
  json += '"seq":"' + this.seq;
  json += '","type":"' + this.type;
  json += '","command":"' + this.command + '"';
  if (this.arguments) {
    json += ',"arguments":' + DebugCommand.toJSON(this.arguments);
  }
  json += '}'
  return json;
}

/**
 * Encode the contents of this message and send it to the debugger.
 * @param {Object} tab - tab being debugged.  This is an internal
 *     Chrome object.
 */
DebugCommand.prototype.sendToDebugger = function(tab) {
  this.seq = DebugCommand.next_seq_++;
  str = this.toJSONProtocol();
  dprint("sending: " + str);
  tab.sendToDebugger(str);
};

DebugCommand.trim = function(str) {
  return str.replace(/^\s*/, '').replace(/\s*$/, '');
};

/**
 * Strip off a trailing parameter after a ':'. As the identifier for the
 * source can contain ':' characters (e.g. 'http://www....) something after
 * a ':' is only considered a parameter if it is numeric. 
 * @return {Array} two element array, the trimmed string and the parameter,
 *   or -1 if no parameter
 */
DebugCommand.stripTrailingParameter = function(str, opt_separator) {
  var sep = opt_separator || ':';
  var index = str.lastIndexOf(sep);
  // If a separator character if found strip if numeric.
  if (index != -1) {
    var value = parseInt(str.substring(index + 1, str.length), 10);
    if (isNaN(value) || value < 0) {
      return [str, -1];
    }
    str = str.substring(0, index);
    return [str, value];
  }
  return [str, -1];
};

/**
 * Format source and location strings based on source location input data.
 * @param {Object} script - script information object
 * @param {String} source - source code for the current location
 * @param {int} line - line number (0-based)
 * @param {String} func - function name
 * @return {array} [location(string), source line(string), line number(int)]
 */
DebugCommand.getSourceLocation = function(script, source, line, func) {
  // source line is 0-based, we present as 1-based
  line++;

  // TODO(erikkay): take column into account as well
  if (source)
    source = "" + line + ": " + source;
  var location = '';
  if (func) {
    location = func + ", ";
  }
  location += script ? script.name : '[no source]';
  return [location, source, line];
};

/**
 * Aliases for debugger commands.
 */
DebugCommand.aliases = {
  'b': 'break',
  'bi': 'break_info',
  'br': 'break',
  'bt': 'backtrace',
  'c': 'continue',
  'f': 'frame',
  'h': 'help',
  '?': 'help',
  'ls': 'source',
  'n': 'next',
  'p': 'print',
  's': 'step',
  'so': 'stepout',
};

/**
 * Parses arguments to "args" and "locals" command, and initializes
 * the underlying DebugCommand (which is a frame request).
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseArgsAndLocals_ = function(str) {
  this.command = "frame";
  return str.length ? -1 : 1;
};

/**
 * Parses arguments to "break_info" command, and executes it.
 * "break_info" has an optional argument, which is the breakpoint
 * identifier.
 * @see DebugCommand.commands
 * @param {string} str - The arguments to be parsed.
 * @return -1 for usage error, 0 for success
 */
DebugCommand.prototype.parseBreakInfo_ = function(str) {
  this.type = "shell";

  // Array of breakpoints to be printed by this command
  // (default to all breakpoints)
  var breakpointsToPrint = shell_.breakpoints;

  if (str.length > 0) {
    // User specified an invalid breakpoint (not a number)
    if (!str.match(/^\s*\d+\s*$/))  
      return -1; // invalid usage

    // Check that the specified breakpoint identifier exists
    var id = parseInt(str);
    var info = shell_.breakpoints[id];
    if (!info) {
      print("Error: Invalid breakpoint");
      return 0; // success (of sorts)
    }
    breakpointsToPrint = [info];
  } else {
    // breakpointsToPrint.length isn't accurate, because of
    // deletions
    var num_breakpoints = 0;
    for (var i in breakpointsToPrint) num_breakpoints++;
    
    print("Num breakpoints: " + num_breakpoints);
  }

  DebugShell.printBreakpoints_(breakpointsToPrint);

  return 0; // success
}

/**
 * Parses arguments to "step" command. 
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseStep_ = function(str, opt_stepaction) {
  this.command = "continue";
  action = opt_stepaction || "in";
  this.arguments = {"stepaction" : action}
  if (str.length) {
    count = parseInt(str);
    if (count > 0) {
      this.arguments["stepcount"] = count;
    } else {
      return -1;
    }
  }
  return 1;
};

/**
 * Parses arguments to "step" command. 
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseStepOut_ = function(str) {
  return this.parseStep_(str, "out");
};

/**
 * Parses arguments to "next" command. 
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseNext_ = function(str) {
  return this.parseStep_(str, "next");
};

/**
 * Parse the arguments to "print" command.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return 1 - always succeeds
 */
DebugCommand.prototype.parsePrint_ = function(str) {
  this.command = "evaluate";
  this.arguments = { "expression" : str };
  // If the page is in the running state, then we force the expression to
  // evaluate in the global context to avoid evaluating in a random context.
  if (shell_.running)
    this.arguments["global"] = true;
  return 1;
};

/**
 * Handle the response to a "print" command and display output to user.
 * @param {ProtocolPacket} evaluate_response - the V8 debugger response object
 */
DebugCommand.responsePrint_ = function(evaluate_response) {
  body = evaluate_response.body();
  if (body['text'] != undefined) {
    print(body['text']);
  } else {
    // TODO(erikkay): is "text" ever not set?
    print("can't print response");
  }
};

/**
 * Parse the arguments to "dir" command.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return 1 - always succeeds
 */
DebugCommand.prototype.parseDir_ = function(str) {
  this.command = "evaluate";
  this.arguments = { "expression" : str };
  // If the page is in the running state, then we force the expression to
  // evaluate in the global context to avoid evaluating in a random context.
  if (shell_.running)
    this.arguments["global"] = true;
  return 1;
};

/**
 * Handle the response to a "dir" command and display output to user.
 * @see http://wiki/Main/V8Debugger
 * @param {ProtocolPacket} evaluate_response - the V8 debugger response object
 */
DebugCommand.responseDir_ = function(evaluate_response) {
  var body = evaluate_response.body();
  if (body.properties) {
    print(body.properties.length + ' properties');
    for (var n in body.properties) {
      var property_info = body.properties[n].name;
      property_info += ': ';
      var value = evaluate_response.lookup(body.properties[n].ref);
      if (value && value.type) {
        property_info += value.type;
      } else {
        property_info += '<no type>';
      }
      property_info += ' (#';
      property_info += body.properties[n].ref;
      property_info += '#)';
      print(property_info);
    }
  }
};

/**
 * Parses arguments to "break" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success, 0 for handled internally
 */
DebugCommand.prototype.parseBreak_ = function(str) {
  function stripTrailingParameter() {
    var ret = DebugCommand.stripTrailingParameter(str, ':');
    str = ret[0];
    return ret[1];
  }
  
  if (str.length == 0) {
    this.command = "break";
    return 1;
  } else {
    var parts = str.split(/\s+/);
    var condition = null;
    if (parts.length > 1) {
      str = parts.shift();
      condition = parts.join(" ");
    }
  
    this.command = "setbreakpoint";

    // Locate ...[:line[:column]] if present.
    var line = -1;
    var column = -1;
    line = stripTrailingParameter();
    if (line != -1) {
      line -= 1;
      var l = stripTrailingParameter();
      if (l != -1) {
        column = line;
        line = l - 1;
      }
    }

    if (line == -1 && column == -1) {
      this.arguments = { 'type' : 'function',
                         'target' : str };
    } else {
      var script = shell_.matchScript(str, line);
      if (script) {
        this.arguments = { 'type' : 'script',
                           'target' : script.name };
      } else {
        this.arguments = { 'type' : 'function',
                           'target' : str };
      }
      this.arguments.line = line;
      if (column != -1)
        this.arguments.position = column;
    }
    if (condition)
      this.arguments.condition = condition;
  }
  return 1;
};

/**
 * Handle the response to a "break" command and display output to user.
 * @param {ResponsePacket} setbreakpoint_response - the V8 debugger response
 *     object
 */
DebugCommand.responseBreak_ = function(setbreakpoint_response) {
  var body = setbreakpoint_response.body();
  var info = new BreakpointInfo(
      parseInt(body.breakpoint),
      setbreakpoint_response.command.arguments.type,
      setbreakpoint_response.command.arguments.target,
      setbreakpoint_response.command.arguments.line,
      setbreakpoint_response.command.arguments.position,
      setbreakpoint_response.command.arguments.condition);
  shell_.addedBreakpoint(info);
};

/**
 * Parses arguments to "backtrace" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
 DebugCommand.prototype.parseBacktrace_ = function(str) {
  if (str.length > 0) {
    var parts = str.split(/\s+/);
    var non_empty_parts = parts.filter(function(s) { return s.length > 0; });
    // We need exactly two arguments.
    if (non_empty_parts.length != 2) {
      return -1;
    }
    var from = parseInt(non_empty_parts[0], 10);
    var to = parseInt(non_empty_parts[1], 10);
    // The two arguments have to be integers.
    if (from != non_empty_parts[0] || to != non_empty_parts[1]) {
      return -1;
    }
    this.arguments = { 'fromFrame': from, 'toFrame': to + 1 };
  } else {
    // Default to fetching the first 10 frames.
    this.arguments = { 'fromFrame': 0, 'toFrame': 10 };
  }
  return 1;
};

/**
 * Handle the response to a "backtrace" command and display output to user.
 * @param {ResponsePacket} backtrace_response - the V8 debugger response object
 */
DebugCommand.responseBacktrace_ = function(backtrace_response) {
  body = backtrace_response.body();
  if (body && body.totalFrames) {
    print('Frames #' + body.fromFrame + ' to #' + (body.toFrame - 1) +
	  ' of ' + body.totalFrames + ":");
    for (var i = 0; i < body.frames.length; i++) {
      print(body.frames[i].text);
    }
  } else {
    print("unimplemented (sorry)");
  }
};


/**
 * Parses arguments to "clear" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseClearCommand_ = function(str) {
  this.command = "clearbreakpoint";
  if (str.length > 0) {
    var i = parseInt(str, 10);
    if (i != str) {
      return -1;
    }
    this.arguments = { 'breakpoint': i };
  }
  return 1;
}

/**
 * Handle the response to a "clear" command and display output to user.
 * @param {ResponsePacket} clearbreakpoint_response - the V8 debugger response
 *     object
 */
DebugCommand.responseClear_ = function(clearbreakpoint_response) {
  var body = clearbreakpoint_response.body();
  shell_.clearedBreakpoint(parseInt(msg.command.arguments.breakpoint));
}


/**
 * Parses arguments to "continue" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseContinueCommand_ = function(str) {
  this.command = "continue";
  if (str.length > 0) {
    return -1;
  }
  return 1;
}

/**
 * Parses arguments to "frame" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseFrame_ = function(str) {
  if (str.length > 0) {
    var i = parseInt(str, 10);
    if (i != str) {
      return -1;
    }
    this.arguments = { 'number': i };
  }
  return 1;
};

/**
 * Handle the response to a "frame" command and display output to user.
 * @param {ResponsePacket} frame_response - the V8 debugger response object
 */
DebugCommand.responseFrame_ = function(frame_response) {
  var body = frame_response.body();
  var func = frame_response.lookup(body.func.ref);
  loc = DebugCommand.getSourceLocation(func.script, 
      body.sourceLineText, body.line, func.name);
  print("#" + (body.index <= 9 ? '0' : '') + body.index + " " + loc[0]);
  print(loc[1]);
  shell_.current_frame = body.index;
  shell_.current_line = loc[2];
  shell_.current_script = func.script;
};

/**
 * Handle the response to a "args" command and display output to user.
 * @param {ProtocolPacket} frame_response - the V8 debugger response object (for
 *     "frame" command)
 */
DebugCommand.responseArgs_ = function(frame_response) {
  var body = frame_response.body();
  DebugCommand.printVariables_(body.arguments, frame_response);
}

/**
 * Handle the response to a "locals" command and display output to user.
 * @param {Object} msg - the V8 debugger response object (for "frame" command)
 */
DebugCommand.responseLocals_ = function(frame_response) {
  var body = frame_response.body();
  DebugCommand.printVariables_(body.locals, frame_response);
}

DebugCommand.printVariables_ = function(variables, protocol_packet) {
  for (var i = 0; i < variables.length; i++) {
    print(variables[i].name + " = " +
        DebugCommand.toPreviewString_(protocol_packet.lookup(variables[i].value.ref)));
  }
}

DebugCommand.toPreviewString_ = function(value) {
  // TODO(ericroman): pretty print arrays and objects, recursively.
  // TODO(ericroman): truncate length of preview if too long?
  if (value.type == "string") {
    // Wrap the string in quote marks and JS-escape
    return DebugCommand.stringToJSON(value.text);
  }
  return value.text;
}

/**
 * Parses arguments to "scripts" command.
 * @see DebugCommand.commands
 * @param {string} str - The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseScripts_ = function(str) {
  return 1
};

/**
 * Handle the response to a "scripts" command and display output to user.
 * @param {ResponsePacket} scripts_response - the V8 debugger response object
 */
DebugCommand.responseScripts_ = function(scripts_response) {
  scripts = scripts_response.body();
  shell_.scripts = [];
  for (var i in scripts) {
    var script = scripts[i];

    // Add this script to the internal list of scripts.
    shell_.scripts.push(script);
    
    // Print result if this response was the result of a user command.
    if (scripts_response.command.from_user) {
      var name = script.name;
      if (name) {
        if (script.lineOffset > 0) {
          print(name + " (lines " + script.lineOffset + "-" +
                (script.lineOffset + script.lineCount - 1) + ")");
        } else {
          print(name + " (lines " + script.lineCount + ")");
        }
      } else {
        // For unnamed scripts (typically eval) display some source.
        var sourceStart = script.sourceStart;
        if (sourceStart.length > 40)
          sourceStart = sourceStart.substring(0, 37) + '...';
        print("[unnamed] (source:\"" + sourceStart + "\")");
      }
    }
  }
};

/**
 * Parses arguments to "source" command.
 * @see DebugCommand.commands
 * @param {string} str - The arguments to be parsed.
 * @return -1 for usage error, 1 for success
 */
DebugCommand.prototype.parseSource_ = function(str) {
  this.arguments = {};
  if (this.current_frame > 0)
    this.arguments.frame = this.current_frame;
  if (str.length) {
    var args = str.split(" ");
    if (args.length == 1) {
      // with 1 argument n, we print 10 lines starting at n
      var num = parseInt(args[0]);
      if (num > 0) {
        this.arguments.fromLine = num - 1;
        this.arguments.toLine = this.arguments.fromLine + 10;
      } else {
        return -1;
      }
    } else if (args.length == 2) {
      // with 2 arguments x and y, we print from line x to line x + y
      var from = parseInt(args[0]);
      var len = parseInt(args[1]);
      if (from > 0 && len > 0) {
        this.arguments.fromLine = from - 1;
        this.arguments.toLine = this.arguments.fromLine + len;
      } else {
        return -1;
      }
    } else {
      return -1;
    }
    if (this.arguments.fromLine < 0)
      return -1;
    if (this.arguments.toLine <= this.arguments.fromLine)
      return -1;
  } else if (shell_.current_line > 0) {
    // with no arguments, we print 11 lines with the current line as the center
    this.arguments.fromLine =
        Math.max(0, shell_.current_line - 6);
    this.arguments.toLine = this.arguments.fromLine + 11;
  }
  return 1;
};

/**
 * Handle the response to a "source" command and display output to user.
 * @param {ProtocolPacket} source_response - the V8 debugger response object
 */
DebugCommand.responseSource_ = function(source_response) {
  var body = source_response.body();
  var from_line = parseInt(body.fromLine) + 1;
  var source = body.source;
  var lines = source.split('\n');
  var maxdigits = 1 + Math.floor(DebugCommand.log10(from_line + lines.length))
  for (var num in lines) {
    // there's an extra newline at the end
    if (num >= (lines.length - 1) && lines[num].length == 0)
      break;
    spacer = maxdigits - (1 + Math.floor(DebugCommand.log10(from_line)))
    var line = "";
    if (from_line == shell_.current_line) {
      for (var i = 0; i < (maxdigits + 2); i++)
        line += ">";
    } else {
      for (var i = 0; i < spacer; i++)
        line += " ";
      line += from_line + ": ";
    }
    line += lines[num];
    print(line);
    from_line++;
  }
};

/**
 * Parses arguments to "help" command.  See DebugCommand.commands below
 * for syntax details.
 * @see DebugCommand.commands
 * @param {string} str The arguments to be parsed.
 * @return 0 for handled internally
 */
DebugCommand.parseHelp_ = function(str) {
  DebugCommand.help(str);
  return 0;
};

/**
 * Takes argument and evaluates it in the context of the shell to allow commands
 * to be escaped to the outer shell.  Used primarily for development purposes.
 * @see DebugCommand.commands
 * @param {string} str The expression to be evaluated
 * @return 0 for handled internally
 */
DebugCommand.parseShell_ = function(str) {
  print(eval(str));
  return 0;
}

DebugCommand.parseShellDebug_ = function(str) {
  shell_.debug = !shell_.debug;
  if (shell_.debug) {
    print("shell debugging enabled");
  } else {
    print("shell debugging disabled");
  }
  return 0;
}

/**
 * Parses a user-entered command string.
 * @param {string} str The arguments to be parsed.
 */
DebugCommand.prototype.parseArgs_ = function(str) {
  if (str.length)
    str = DebugCommand.trim(str); 
  var cmd = DebugCommand.commands[this.user_command];
  if (cmd) {
    var parse = cmd['parse'];
    if (parse == undefined) {
      print('>>>can\'t find parse func for ' + this.user_command);
      this.type = "error";
    } else {
      var ret = parse.call(this, str);
      if (ret > 0) {
        // Command gererated a debugger request.
        this.type = "request";
      } else if (ret == 0) {
        // Command handeled internally.
        this.type = "handled";
      } else if (ret < 0) {
        // Command error.
        this.type = "handled";
        DebugCommand.help(this.user_command);
      }
    }
  } else {
    this.type = "handled";
    print('unknown command: ' + this.user_command);
    DebugCommand.help();
  }
};

/**
 * Displays command help or all help.
 * @param {string} opt_str Which command to print help for.
 */
DebugCommand.help = function(opt_str) {
  if (opt_str) {
    var cmd = DebugCommand.commands[opt_str];
    var usage = cmd.usage;
    print('usage: ' + usage);
    // Print additional details for the command.
    if (cmd.help) {
      print(cmd.help);
    }
  } else {
    if (shell_.running) {
      print('Status: page is running');
    } else {
      print('Status: page is paused');
    }
    print('Available commands:');
    for (var key in DebugCommand.commands) {
      var cmd = DebugCommand.commands[key];
      if (!cmd['hidden'] && (!shell_.running || cmd['while_running'])) {
        var usage = cmd.usage;
        print('   ' + usage);
      }
    }
  }
};

/**
 * Valid commands, their argument parser and their associated usage text.
 */
DebugCommand.commands = {
  'args': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, 
            'usage': 'args',
            'help': 'summarize the arguments to the current function.',
            'response': DebugCommand.responseArgs_ },
  'break': { 'parse': DebugCommand.prototype.parseBreak_, 
             'response': DebugCommand.responseBreak_,
             'usage': 'break [location] <condition>',
             'help': 'location is one of <function> | <script:function> | <script:line> | <script:line:pos>',
             'while_running': true },
  'break_info': { 'parse': DebugCommand.prototype.parseBreakInfo_, 
                  'usage': 'break_info [breakpoint #]',
                  'help': 'list the current breakpoints, or the details on a single one',
                  'while_running': true },
  'backtrace': { 'parse': DebugCommand.prototype.parseBacktrace_,
                 'response': DebugCommand.responseBacktrace_,
                 'usage': 'backtrace [from frame #] [to frame #]' },  
  'clear': { 'parse': DebugCommand.prototype.parseClearCommand_, 
             'response': DebugCommand.responseClear_,
             'usage': 'clear <breakpoint #>',
             'while_running': true },
  'continue': { 'parse': DebugCommand.prototype.parseContinueCommand_, 
                'usage': 'continue' },
  'dir': { 'parse': DebugCommand.prototype.parseDir_,
             'response': DebugCommand.responseDir_,
             'usage': 'dir <expression>',
             'while_running': true },
  'frame': { 'parse': DebugCommand.prototype.parseFrame_, 
             'response': DebugCommand.responseFrame_,
             'usage': 'frame <frame #>' },
  'help': { 'parse': DebugCommand.parseHelp_, 
            'usage': 'help [command]',
            'while_running': true },
  'locals': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, 
              'usage': 'locals',
              'help': 'summarize the local variables for current frame',
              'response': DebugCommand.responseLocals_ },
  'next': { 'parse': DebugCommand.prototype.parseNext_, 
            'usage': 'next' } ,
  'print': { 'parse': DebugCommand.prototype.parsePrint_,
             'response': DebugCommand.responsePrint_,
             'usage': 'print <expression>',
             'while_running': true },
  'scripts': { 'parse': DebugCommand.prototype.parseScripts_,
              'response': DebugCommand.responseScripts_,
              'usage': 'scripts',
              'while_running': true },
  'source': { 'parse': DebugCommand.prototype.parseSource_,
               'response': DebugCommand.responseSource_,
               'usage': 'source [from line] | [<from line> <num lines>]' },
  'step': { 'parse': DebugCommand.prototype.parseStep_, 
            'usage': 'step' },
  'stepout': { 'parse': DebugCommand.prototype.parseStepOut_,
               'usage': 'stepout' },
  // local eval for debugging - remove this later
  'shell': { 'parse': DebugCommand.parseShell_,
             'usage': 'shell <expression>',
             'while_running': true,
             'hidden': true },
  'shelldebug': { 'parse': DebugCommand.parseShellDebug_,
                  'usage': 'shelldebug',
                  'while_running': true,
                  'hidden': true },
};


/**
 * Debug shell using the new JSON protocol
 * @param {Object} tab - which tab is to be debugged.  This is an internal
 *     Chrome object.
 * @constructor
 */
function DebugShell(tab) {
  this.tab = tab;
  this.tab.attach();
  this.ready = true;
  this.running = true;
  this.current_command = undefined;
  this.pending_commands = [];
  // The auto continue flag is used to indicate whether the JavaScript execution
  // should automatically continue after a break event and the processing of
  // pending commands. This is used to make it possible for the user to issue
  // commands, e.g. setting break points, without making an explicit break. In
  // this case the debugger will silently issue a forced break issue the command
  // and silently continue afterwards.
  this.auto_continue = false;
  this.debug = false;
  this.current_line = -1;
  this.current_pos = -1;
  this.current_frame = 0;
  this.current_script = undefined;
  this.scripts = [];

  // Mapping of breakpoints id --> info.
  // Must use numeric keys.
  this.breakpoints = [];
};

DebugShell.prototype.set_ready = function(ready) {
  if (ready != this.ready) {
    this.ready = ready;
    ChromeNode.setDebuggerReady(this.ready);
  }
};

DebugShell.prototype.set_running = function(running) {
  if (running != this.running) {
    this.running = running;
    ChromeNode.setDebuggerBreak(!this.running);
  }
};

/**
 * Execute a constructed DebugCommand object if possible, otherwise pend.
 * @param cmd {DebugCommand} - command to execute
 */
DebugShell.prototype.process_command = function(cmd) {
  dprint("Running: " + (this.running ? "yes" : "no"));

  // The "break" commands needs to be handled seperatly
  if (cmd.command == "break") {
    if (this.running) {
      // Schedule a break.
      print("Stopping JavaScript execution...");
      this.tab.debugBreak(false);
    } else {
      print("JavaScript execution already stopped.");
    }
    return;
  }
  
  // If page is running an break needs to be issued.
  if (this.running) {
    // Some requests are not valid when the page is running.
    var cmd_info = DebugCommand.commands[cmd.user_command];
    if (!cmd_info['while_running']) {
      print(cmd.user_command + " can only be run while paused");
      return;
    }

    // Add the command as pending before scheduling a break.    
    this.pending_commands.push(cmd);
    dprint("pending command: " + cmd.toJSONProtocol());

    // Schedule a forced break and enable auto continue.
    this.tab.debugBreak(true);
    this.auto_continue = true;
    this.set_ready(false);
    return;
  }
  
  // If waiting for a response add command as pending otherwise send the
  // command.
  if (this.current_command) {
    this.pending_commands.push(cmd);
    dprint("pending command: " + cmd.toJSONProtocol());
  } else {
    this.current_command = cmd;
    cmd.sendToDebugger(this.tab);
    this.set_ready(false);
  }
};

/**
 * Handle a break event from the debugger.
 * @param msg {ResponsePacket} - break_event protocol message to handle
 */
DebugShell.prototype.event_break = function(break_event) {
  this.current_frame = 0;
  this.set_running(false);
  var body = break_event.body();
  if (body) {
    this.current_script = body.script;
    var loc = DebugCommand.getSourceLocation(body.script,
        body.sourceLineText, body.sourceLine, body.invocationText);
    var location = loc[0];
    var source = loc[1];
    this.current_line = loc[2];
    if (body.breakpoints) {
      // Always disable auto continue if a real break point is hit.
      this.auto_continue = false;
      var breakpoints = body.breakpoints;
      print("paused at breakpoint " + breakpoints.join(",") + ": " + 
            location);
      for (var i = 0; i < breakpoints.length; i++)
        this.didHitBreakpoint(parseInt(breakpoints[i]));
    } else if (body.scriptData == "") {
      print("paused");
    } else {
      // step, stepout, next, "break" and a "debugger" line in the code
      // are all treated the same (they're not really distinguishable anyway)
      if (location != this.last_break_location) {
        // We only print the location (function + script) when it changes, 
        // so as we step, you only see the source line when you transition
        // to a new script and/or function. Also if auto continue is enables
        // don't print the break location.
        if (!this.auto_continue)
          print(location);
      }
    }
    // Print th current source line unless auto continue is enabled.
    if (source && !this.auto_continue)
      print(source);
    this.last_break_location = location;
  }
  if (!this.auto_continue)
    this.set_ready(true);
};

/**
 * Handle an exception event from the debugger.
 * @param msg {ResponsePacket} - exception_event protocol message to handle
 */
DebugShell.prototype.event_exception = function(exception_event) {
  this.set_running(false);
  this.set_ready(true);
  var body = exception_event.body();
  if (body) {
    if (body["uncaught"]) {
      print("uncaught exception " + body["exception"].text);
    } else {
      print("paused at exception " + body["exception"].text);
    }
  }
};

DebugShell.prototype.matchScript = function(script_match, line) {
  var script = null;
  // In the v8 debugger, all scripts have a name, line offset and line count
  // Script names are usually URLs which are a pain to have to type again and
  // again, so we match the tail end of the script name.  This makes it easy
  // to type break foo.js:23 rather than
  // http://www.foo.com/bar/baz/quux/test/foo.js:23.  In addition to the tail
  // of the name we also look at the lines the script cover.  If there are
  // several scripts with the same tail including the requested line we match
  // the first one encountered.
  // TODO(sgjesse) Find how to handle several matching scripts.
  var candidate_scripts = [];
  for (var i in this.scripts) {
    if (this.scripts[i].name &&
        this.scripts[i].name.indexOf(script_match) >= 0) {
      candidate_scripts.push(this.scripts[i]);
    }
  }
  for (var i in candidate_scripts) {
    var s = candidate_scripts[i];
    var from = s.lineOffset;
    var to = from + s.lineCount;
    if (from <= line && line < to) {
      script = s;
      break;
    }
  }
  if (script)
    return script;
  else
    return null;
}

// The Chrome Subshell interface requires:
// prompt(), command(), response(), exit() and on_disconnect()

/** 
 * Called by Chrome Shell to get a prompt string to display.
 */
DebugShell.prototype.prompt = function() {
  if (this.current_command)
    return '';
  if (!this.running)
    return 'v8(paused)> ';
  else
    return 'v8(running)> ';
};

/**
 * Called by Chrome Shell when command input has been received from the user.
 */
DebugShell.prototype.command = function(str) {
  if (this.tab) {
    str = DebugCommand.trim(str);
    if (str.length) {
      var cmd = new DebugCommand(str);
      cmd.from_user = true;
      if (cmd.type == "request")
        this.process_command(cmd);
    }
  } else {
    print(">>not connected to a tab");
  }
};

/**
 * Called by Chrome Shell when a response to a previous command has been
 * received.
 * @param {Object} msg Message object.
 */
DebugShell.prototype.response = function(msg) {
  dprint("received: " + (msg && msg.type));
  var response;
  try {
    response = new ProtocolPackage(msg);
  } catch (error) {
    print(error.toString(), str);
    return;
  }
  if (response.type() == "event") {
    ev = response.event();
    if (ev == "break") {
      this.event_break(response);
    } else if (ev == "exception") {
      this.event_exception(response);
    }
  } else if (response.type() == "response") {
    if (response.requestSeq() != undefined) {
      if (!this.current_command || this.current_command.seq != response.requestSeq()){
        throw("received response to unknown command " + str);
      }
    } else {
      // TODO(erikkay): should we reject these when they happen?
      print(">>no request_seq in response " + str);
    }
    var cmd = DebugCommand.commands[this.current_command.user_command]
    response.command = this.current_command;
    this.current_command = null
    this.set_running(response.running());
    if (!response.success()) {
      print(response.message());
    } else {
      var handler = cmd['response'];
      if (handler != undefined) {
        handler.call(this, response);
      }
    }
    this.set_ready(true);
  }

  // Process next pending command if any.
  if (this.pending_commands.length) {
    this.process_command(this.pending_commands.shift());
  } else if (this.auto_continue) {
    // If no more pending commands and auto continue is active issue a continue command.
    this.auto_continue = false;
    this.process_command(new DebugCommand("continue"));
  }
};

/**
 * Called when a breakpoint has been set.
 * @param {BreakpointInfo} info - details of breakpoint set.
 */
DebugShell.prototype.addedBreakpoint = function(info) {
  print("set breakpoint #" + info.id);
  this.breakpoints[info.id] = info;
}

/**
 * Called when a breakpoint has been cleared.
 * @param {int} id - the breakpoint number that was cleared.
 */
DebugShell.prototype.clearedBreakpoint = function(id) {
  assertIsNumberType(id, "clearedBreakpoint called with invalid id");

  print("cleared breakpoint #" + id);
  delete this.breakpoints[id];
}

/**
 * Called when a breakpoint has been reached.
 * @param {int} id - the breakpoint number that was hit.
 */
DebugShell.prototype.didHitBreakpoint = function(id) {
  assertIsNumberType(id, "didHitBreakpoint called with invalid id");

  var info = this.breakpoints[id];
  if (!info)
    throw "Could not find breakpoint #" + id;

  info.hit_count ++;
}

/**
 * Print a summary of the specified breakpoints.
 *
 * @param {Array<BreakpointInfo>} breakpointsToPrint - List of breakpoints. The
 *     index is unused (id is determined from the info).
 */
DebugShell.printBreakpoints_ = function(breakpoints) {
  // TODO(ericroman): this would look much nicer if we could output as an HTML
  // table. I tried outputting as formatted text table, but this looks aweful
  // once it triggers wrapping (which is very likely if the target is a script)

  // Output as a comma separated list of key=value
  for (var i in breakpoints) {
    var b = breakpoints[i];
    var props = ["id", "hit_count", "type", "target", "line", "position",
        "condition"];
    var propertyList = [];
    for (var i = 0; i < props.length; i++) {
      var prop = props[i];
      var val = b[prop];
      if (val != undefined)
        propertyList.push(prop + "=" + val);
    }
    print(propertyList.join(", "));
  }
}

/**
 * Called by Chrome Shell when the outer shell is detaching from debugging
 * this tab.
 */
DebugShell.prototype.exit = function() {
  if (this.tab) {
    this.tab.detach();
    this.tab = null;
  }
};

/**
 * Called by the Chrome Shell when the tab that the shell is debugging
 * have attached.
 */
DebugShell.prototype.on_attach = function(title) {
  if (!title)
    title = "Untitled";
  print('attached to ' + title);
  // on attach, we update our current script list
  var cmd = new DebugCommand("scripts");
  cmd.from_user = false;
  this.process_command(cmd);
};


/**
 * Called by the Chrome Shell when the tab that the shell is debugging
 * went away.
 */
DebugShell.prototype.on_disconnect = function() {
  print(">>lost connection to tab");
  this.tab = null;
};


/**
 * Protocol packages send from the debugger.
 * @param {string} json - raw protocol packet as JSON string.
 * @constructor
 */
function ProtocolPackage(msg) {
  this.packet_ = msg;
  this.refs_ = [];
  if (this.packet_.refs) {
    for (var i = 0; i < this.packet_.refs.length; i++) {
      this.refs_[this.packet_.refs[i].handle] = this.packet_.refs[i];
    }
  }
}


/**
 * Get the packet type.
 * @return {String} the packet type
 */
ProtocolPackage.prototype.type = function() {
  return this.packet_.type;
}


/**
 * Get the packet event.
 * @return {Object} the packet event
 */
ProtocolPackage.prototype.event = function() {
  return this.packet_.event;
}


/**
 * Get the packet request sequence.
 * @return {number} the packet request sequence
 */
ProtocolPackage.prototype.requestSeq = function() {
  return this.packet_.request_seq;
}


/**
 * Get the packet request sequence.
 * @return {number} the packet request sequence
 */
ProtocolPackage.prototype.running = function() {
  return this.packet_.running ? true : false;
}


ProtocolPackage.prototype.success = function() {
  return this.packet_.success ? true : false;
}


ProtocolPackage.prototype.message = function() {
  return this.packet_.message;
}


ProtocolPackage.prototype.body = function() {
  return this.packet_.body;
}


ProtocolPackage.prototype.lookup = function(handle) {
  return this.refs_[handle];
}


/**
 * Structure that holds the details about a breakpoint.
 * @constructor
 *
 * @param {int} id - breakpoint number
 * @param {string} type - "script" or "function"
 * @param {string} target - either a function name, or script url
 * @param {int} line - line number in the script, or undefined
 * @param {int} position - column in the script, or undefined
 * @param {string} condition - boolean expression, or undefined
 */
function BreakpointInfo(id, type, target, line, position, condition) {
  this.id = id;
  this.type = type;
  this.target = target;

  if (line != undefined)
    this.line = line;
  if (position != undefined)
    this.position = position;
  if (condition != undefined)
    this.condition = condition;

  this.hit_count = 0;

  // Check that the id is numeric, otherwise will run into problems later
  assertIsNumberType(this.id, "id is not a number");
}

var shell_ = null;
DebugShell.initDebugShell = function(debuggerUI) {
  if (!DebugShell.singleton) {
    DebugShell.ui = debuggerUI;
    DebugShell.singleton = new DebugShell(TabNode);
    shell_ = DebugShell.singleton;
    
    // enable debug output
    //shell_.debug = true;
  }
};

/**
 * Print debugging message when DebugShell's debug flag is true.
 */
function dprint(str) {
  if (shell_ && shell_.debug) {
    print(str);  
  }
};

/**
 * Helper that throws error if x is not a number
 * @param x {object} - object to test type of
 * @param error_message {string} - error to throw on failure
 */
function assertIsNumberType(x, error_message) {
  if (typeof x != "number")
    throw error_message;
}

////////////////////// migration staff //////////////////////////
// This file was copied from chrome\browser\debugger\resources\debugger_shell.js

function print(txt) {
  var ui = DebugShell.ui;
  if (ui) {
    ui.appendText(txt);
  }
}

var TabNode = {
  debugBreak: function(force) {
    DebuggerIPC.sendMessage(["debugBreak", force]);
  },
  attach: function() {
    DebuggerIPC.sendMessage(["attach"]);
  },
  detach: function() {
    // TODO(yurys): send this from DebugHandler when it's being destroyed?
    DebuggerIPC.sendMessage(["detach"]);
  },
  sendToDebugger: function(str) {
    DebuggerIPC.sendMessage(["sendToDebugger", str]);
  }
};

var ChromeNode = {
  setDebuggerReady: function(isReady) {
    DebuggerIPC.sendMessage(["setDebuggerReady", isReady]);
  },
  setDebuggerBreak: function(isBreak) {
    var ui = DebugShell.ui;
    if (ui) {
      ui.setDebuggerBreak(isBreak);
    }
    DebuggerIPC.sendMessage(["setDebuggerBreak", isBreak]);
  }
};