/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is virtual:// protocol handling, file service.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// Get JS modules.
Components.utils.import("resource://app/modules/XPCOMUtils.jsm");
Components.utils.import("resource://app/modules/ArrayConverter.jsm");
Components.utils.import("resource://app/modules/ecma-debug.jsm");

/* Begin static code. */

// Utility function.
function ERROR_FAILURE() {
  throw Components.results.NS_ERROR_FAILURE;
}

// Utility function.
function NOT_IMPLEMENTED() {
  throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

// Utility function.
function ERROR_ABORT() {
  throw Components.results.NS_ERROR_ABORT;
}

// Utility function
function NOT_INITIALIZED() {
  throw Components.results.NS_ERROR_NOT_INITIALIZED;
}

// Utility function
function ALREADY_INITIALIZED() {
  throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
}

const C_i = Components.interfaces;

const xeIVirtualFileService       = C_i.xeIVirtualFileService;
const nsIStreamListener           = C_i.nsIStreamListener;
const nsIRequestObserver          = C_i.nsIRequestObserver;
const nsIRequest                  = C_i.nsIRequest;
const nsIChannel                  = C_i.nsIChannel;
const nsIIOService                = C_i.nsIIOService;
const nsIProtocolHandler          = C_i.nsIProtocolHandler;
const nsIExternalProtocolHandler  = C_i.nsIExternalProtocolHandler;
const nsIMultiPartChannel         = C_i.nsIMultiPartChannel;
const nsICachingChannel           = C_i.nsICachingChannel;
const nsIFileChannel              = C_i.nsIFileChannel;
const nsIHttpChannel              = C_i.nsIHttpChannel;
const nsIWyciwygChannel           = C_i.nsIWyciwygChannel;
const nsIHttpChannelInternal      = C_i.nsIHttpChannelInternal;
const nsIInputStreamPump          = C_i.nsIInputStreamPump;
const nsIPipe                     = C_i.nsIPipe;
const nsIInputStream              = C_i.nsIInputStream;
const nsIStringInputStream        = C_i.nsIStringInputStream;
const nsIStandardURL              = C_i.nsIStandardURL;
const nsIURI                      = C_i.nsIURI;
const nsIWritablePropertyBag2     = C_i.nsIWritablePropertyBag2;
const mozIJSSubScriptLoader       = C_i.mozIJSSubScriptLoader;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsISupports                 = C_i.nsISupports;

const ioService = Components.classes["@mozilla.org/network/io-service;1"]
                            .getService(nsIIOService);

const VirtualFileService = {
  _docPack: null,

  // xeIVirtualFileService
  convertURIToVirtual: function convertURIToVirtual(aPath) {
    // http://foo/bar/baz -> virtual://http//foo/bar/baz
    var colon = aPath.indexOf(":");
    var baseURL;
    if (colon != -1) {
      baseURL = aPath.substr(0, colon) + aPath.substr(colon + 1);
    } else {
      baseURL = aPath;
    }

    return "virtual://" + baseURL;
  },

  // xeIVirtualFileService
  resolveVirtualURI: function resolveVirtualURI(aURI) {
    var reResults = /^virtual:\/\/([^#]*)#?.*/.exec(aURI);
    if (!reResults) {
      // MALFORMED_URI would be better, but it doesn't exist here.
      ERROR_FAILURE();
    }

    var realURI = reResults[1];
    // Put the colon back in.
    realURI = realURI.replace(/\//, ":/");
    dump("realURI: " + realURI + "\n");
    return realURI;
  },

  // xeIVirtualFileService
  getWrapperByURI: function getWrapperByURI(aPath) {
    if (!this._docPack) {
      ERROR_ABORT();
    }
    return this._docPack.getWrapper(aPath);
  },

  // xeIVirtualFileService
  setDocumentPack: function setDocumentPack(aPack) {
    this._docPack = aPack;
  },


  _interfaces: [xeIVirtualFileService,
                      nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  classDescription: "Verbosio Virtual File Service",

  // nsIClassInfo
  classID: Components.ID("{050ae4ef-d93f-4fb8-8bd1-ad3ae2d1de1a}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/virtual-file-service;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON, // service

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support
    if (aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

    // Discover what interfaces we need
    var i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE: FileService " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: FileService Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

function VirtualChannel() {
  this.mOriginalURI = null;
  this.mUrl = null;
  this.mName = "";
  this.mOpened = false;
  this.mPump = null;
  this.mDataStream = null;

  // set through asyncOpen
  this.mListener = null;

  // nsIChannel
  this.notificationCallbacks = null;

  // nsIChannel
  this.contentType = null;

  // nsIChannel
  this.owner = null;

  // nsIChannel
  this.contentCharset = "";

  // This is set late in the originalURI setter.
  // nsIChannel
  this.contentLength = -1;

  // nsIRequest
  this.loadGroup = null;
}
VirtualChannel.prototype = {
  // nsIRequest
  get name() {
    return this.mUrl ? this.mUrl.spec : "";
  },

  // nsIRequest
  isPending: function isPending() {
    return this.mPump.isPending();
  },

  // nsIRequest
  get status() {
    return this.mPump.status;
  },

  // nsIRequest
  cancel: function cancel(aStatus) {
    return this.mPump.cancel(aStatus);
  },

  // nsIRequest
  suspend: function suspend() {
    return this.mPump.suspend();
  },

  // nsIRequest
  resume: function resume() {
    return this.mPump.resume();
  },

  // nsIRequest
  get loadFlags() {
    return this.mPump.loadFlags;
  },

  // nsIRequest
  set loadFlags(val) {
    this.mPump.loadFlags = val;
    return val;
  },

  // nsIChannel
  get originalURI() {
    return this.mOriginalURI ? this.mOriginalURI : this.mUrl;
  },

  // nsIChannel
  set originalURI(val) {
    if (this.mOriginalURI) {
      this.mOriginalURI = val;
    }
    return val;
  },

  /**
   * Initialize the channel.
   *
   * @param val Path of the channel to initialize.
   */
  init: function init(val) {
    // Initialize!  First, the actual setter.
    this.mOriginalURI = val;
    this.mUrl = val;

    var spec = val.spec;
    var realURI = VirtualFileService.resolveVirtualURI(val.spec);

    var wrapper = VirtualFileService.getWrapperByURI(realURI);
    var dataBuffer = wrapper.wrappedSource;
    if (!dataBuffer) {
      // We're referring to a file that doesn't exist in the virtual file system.
      // Someone screwed up to call on us if that's the case!
      dump(realURI + " not in virtual file system\n");
      ERROR_FAILURE();
    }

    this.contentType = wrapper.contentType;

    const strInputContract = "@mozilla.org/io/string-input-stream;1";
    this.mDataStream = Components.classes[strInputContract]
                                 .createInstance(nsIStringInputStream);
    this.mDataStream.setData(dataBuffer, dataBuffer.length);
    this.mPump = Components.classes["@mozilla.org/network/input-stream-pump;1"]
                           .createInstance(nsIInputStreamPump);
  },

  // nsIChannel
  get URI() {
    return this.mUrl;
  },

  // nsIChannel
  open: function open() {
    this.mOpened = true;
    return this.mDataStream;
  },

  // nsIChannel
  asyncOpen: function asyncOpen(aListener, aContext) {
    this.mListener = aListener;
    this.mOpened = true;

    this.mPump.init(this.mDataStream, -1, -1, 0, 0, false);

    if (this.loadGroup)
      this.loadGroup.addRequest(this, aContext);

    this.mPump.asyncRead(this, aContext);
  },

  /* nsIChannel properties implemented in constructor
   * notificationCallbacks
   * contentType
   * contentCharset
   * contentLength
   * owner
   */

  // nsIChannel
  get securityInfo() {
    return null;
  },

  // nsIRequestObserver
  onStartRequest: function onStartRequest(aRequest, aContext) {
    if (this.mListener)
      this.mListener.onStartRequest(this, aContext);
  },

  // nsIRequestObserver
  onStopRequest: function onStopRequest(aRequest, aContext, aStatus) {
    if (this.mListener)
      this.mListener.onStopRequest(this, aContext, aStatus);

    this.mListener = null;

    if (this.loadGroup)
      this.loadGroup.removeRequest(this, aContext, aStatus);

    this.notificationCallbacks = null;
  },

  // nsIStreamListener
  onDataAvailable:
  function onDataAvailable(request, ctxt, input, offset, count) {
    if (this.mListener)
      this.mListener.onDataAvailable(this, ctxt, input, offset, count);
  },

  _interfaces: [nsIChannel,
                nsIRequest,
                nsIRequestObserver,
                nsIStreamListener,
                nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  classDescription: "virtual: channel",

  // nsIClassInfo
  classID: Components.ID("{3df533c9-e1e1-460e-8b4c-8049a23418bf}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/virtual-protocol-channel;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0, /* nsIClassInfo.SINGLETON for services */

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support
    if (aIID.equals(nsIMultiPartChannel) ||
        aIID.equals(nsICachingChannel) ||
        aIID.equals(nsIHttpChannel) ||
        aIID.equals(nsIFileChannel) ||
        aIID.equals(nsIWyciwygChannel) ||
        aIID.equals(nsIHttpChannelInternal) ||
        aIID.equals(nsIWritablePropertyBag2) ||
        aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

    // Discover what interfaces we need
    var i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE: VirtualChannel " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: VirtualChannel Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

const VirtualProtocol = {
  // nsIProtocolHandler
  get scheme() {
    return "virtual";
  },

  // nsIProtocolHandler
  get protocolFlags() {
    return nsIProtocolHandler.URI_STD;
  },

  /* While the newURI function should act like nsChromeHandler::NewURI,
   * the newChannel function should act like nsDataHandler::NewChannel.
   */

  // nsIProtocolHandler
  newURI: function newURI(aSpec, aOriginCharset, aBaseURI) {
    var url = Components.classes["@mozilla.org/network/standard-url;1"]
                        .createInstance(nsIStandardURL);
    url.init(nsIStandardURL.URLTYPE_STANDARD, -1, aSpec, aOriginCharset, aBaseURI);
    return url.QueryInterface(nsIURI);
  },

  // nsIProtocolHandler
  newChannel: function newChannel(aURI) {
    // Ensure the URI really exists in our virtual file system!
    var realURI = VirtualFileService.resolveVirtualURI(aURI.spec);

    // Get the source!
    var wrapper = VirtualFileService.getWrapperByURI(realURI);

    // Get the channel.
    var channel = null;
    if (wrapper) {
      // Use virtual: channel
      channel = new VirtualChannel();
      channel.init(aURI);
    } else {
      // Oh, no!  It *wasn't* in our virtual file system.  Get the right channel.
      channel = ioService.newChannel(realURI, aURI.originCharset, null);
    }
    return channel;
  },

  // nsIProtocolHandler
  allowPort: function allowPort(port, scheme) {
    return false;
  },

  _interfaces: [nsIProtocolHandler,
                nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  classDescription: "virtual: protocol handler",

  // nsIClassInfo
  classID: Components.ID("{d0d3483e-f474-48bf-92b1-b0629eadc61f}"),

  // nsIClassInfo
  contractID: "@mozilla.org/network/protocol;1?name=virtual",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: nsIClassInfo.SINGLETON, // service

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support
    if (aIID.equals(nsIExternalProtocolHandler) ||
        aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

    // Discover what interfaces we need
    var i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE: VirtualProtocol " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: VirtualProtocol Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
};

var NSGetModule = XPCOMUtils.generateNSGetModule([
  wrapServiceComponent(VirtualFileService),

  /* ajvincent We don't want to add VirtualChannel, because only
     VirtualProtocol.newChannel should ever create one.
  */

  wrapServiceComponent(VirtualProtocol)
], null, null);
