text: define(["backbone", "underscore", "panelPort", "utils"], function(Backbone, _, panelPort, utils) {
      var inspectedPageClient = new (function() {
          _.extend(this, Backbone.Events);
  
          this.initialize = function() {
              // true when the injection process is being executed, tipically used by the router
              // to decide whether to reload the panel or not.
              this.isInjecting = false;
  
              // Turn inspected page messages into Backbone events,
              // so that Backbone.Events methods like the useful "listenTo" can be used
              panelPort.onMessage.addListener(_.bind(function(message) {
                  if (message && message.target == "page") {
                      this.trigger(message.name, message);
                  }
              }, this));
          };
  
          // Call the callback with an array containing the page frames.
          // A frame is an object {url: frameURL}
          this.getFrames = function(callback) {
              chrome.devtools.inspectedWindow.getResources(_.bind(function(resources) {
                  var frames = [];
                  for (var i=0,l=resources.length; i<l; i++) {
                      var resource = resources[i];
                      if (resource.type == 'document') {
                          frames.push({url: resource.url});
                      }
                  }
                  callback(frames);
              }, this));
          };
  
          // Execute the "func" function in the inspected page,
          // passing to it the arguments specified in the "args" array (that must be JSON-compatible),
          // a more specific context can be setted by using the "context" parameter.
          // frameURL is an optional parameter stating the url of the page frame in which to execute
          // the function, if omitted, the top frame will be used.
          // The callback "onExecuted" is called with the function return value.
          // The method is implemented by using devtools.inspectedWindow.eval.
          this.execFunction = function(func, args, onExecuted, context, frameURL) {
              if (context === undefined) { context = "this"; }
  
              var options = frameURL ? {frameURL: frameURL} : undefined;
  
              var evalCode = "("+func.toString()+").apply("+context+", "+JSON.stringify(args)+");";
              chrome.devtools.inspectedWindow.eval(evalCode, options, 
              function(result, isException) { // on executed
                  if (isException) {
                      var error = _.isObject(isException) ? isException.value : result;
                      console.log({error: error, evalCode: evalCode, args: args});
                      throw error;
                  } else {
                      if (onExecuted) onExecuted(result);
                  }
              });
          };
  
          this.sendMessage = function(messageName, frameURL, messageData) {
              messageName = 'client:'+messageName;
  
              panelPort.postMessage({
                  target: 'extension',
                  timestamp: new Date().getTime(),
                  name: messageName,
                  data: messageData,
                  frameURL: frameURL
              });
          };
  
          // Call the callback when the inspected page (top frame) DOM is fully loaded
          // or right away if that is already true.
          this.ready = function(onReady) {
              this.execFunction(function() {
                  var readyState = document.readyState;
                  return (readyState != "uninitialized" && readyState != "loading");
              }, [], _.bind(function(hasLoaded) { // on executed
                  if (hasLoaded) {
                      onReady();
                  } else {
                      this.once("ready", onReady);
                      // (the event is sent only by the top frame, no need to check the frame url)
                  }
              }, this));
          };
  
          // Reload the inspected page injecting at the beginning of each of its frame
          // the scripts whose absolute base path is specified in the "scriptsBasePath" string.
          // The scripts base directory must contain an index.json file with
          // a scripts array which will be injected in the provided order.
          // Note: the urls are considered as relative to the base path.
          // "options.injectionData" is an optional JSON-compatible hash accessible to the scripts,
          // is tipically used to pass special data not directly accessible from the page,
          // such as the extension url, or for scripts configuration options.
          // - preprocessorURL is an optional parameter stating the absolute url of the preprocessor.
          this.reloadInjecting = function(scriptsBasePath, preprocessorURL, options) {
              options = options || {};
  
              var injectionData = options.injectionData || {};
  
              var inject = _.bind(function(injectedScript, preprocessorScript) {
                  // Reload the inspected page with the code to inject at the beginning of it
                  this.isInjecting = true;
                  chrome.devtools.inspectedWindow.reload({
                      ignoreCache: true, // avoid to load the old and possibly different 
                                         // cached version of the inspected page
                      injectedScript: injectedScript,
                      preprocessorScript: preprocessorScript
                  });
              }, this);
  
              utils.fetchData(["index.json"], scriptsBasePath, _.bind(function(data) {
                  var index = JSON.parse(data[0]);
  
                  var scriptURLs = index.scripts;
                  utils.fetchData(scriptURLs, scriptsBasePath, _.bind(function(scripts) { // on complete
                      // prepare code to inject
                      // TODO: create and use source map to ease debugging
  
                      var injectedScript = '(function(injectionData) {' + '\n\n'
                                         + scripts.join('\n\n') + '\n\n'
                                         + '})('+JSON.stringify(injectionData)+');' + '\n'; // last "\n" prevents eventual EOF error
  
                      if (!preprocessorURL) inject(injectedScript);
                      else utils.fetchData([preprocessorURL], null, _.partial(inject, injectedScript), true);
                  }, this), true);
              }, this), true); // disable request caching (avoid to load the old and possibly different cached
                        // version of the injected scripts), not needed in production.
          };
  
          // a plain inspected page reload without injecting anything,
          // useful for stopping the debug mode (causes removal of backbone agent)
          this.reload = function() {
              chrome.devtools.inspectedWindow.reload();
          };
  
          this.initialize();
      })();
      return inspectedPageClient;
  });
  