/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */

const nsISupports             = Components.interfaces.nsISupports;

const nsIComponentRegistrar   = Components.interfaces.nsIComponentRegistrar;
const nsIFactory              = Components.interfaces.nsIFactory;
const nsIModule               = Components.interfaces.nsIModule;
const nsIProtocolHandler      = Components.interfaces.nsIProtocolHandler;

const nsIFileInputStream      = Components.interfaces.nsIFileInputStream;
const nsIChannel              = Components.interfaces.nsIChannel;
const nsIInputStreamChannel   = Components.interfaces.nsIInputStreamChannel;
const nsILocalFile            = Components.interfaces.nsILocalFile;
const nsIPipe                 = Components.interfaces.nsIPipe;
const nsIStringInputStream    = Components.interfaces.nsIStringInputStream;
const nsIURI                  = Components.interfaces.nsIURI;
const nsIWindowMediator       = Components.interfaces.nsIWindowMediator;

const arIUnMHTProtocolHandler = Components.interfaces.arIUnMHTProtocolHandler;

const nsIIDNService           = Components.interfaces.nsIIDNService;
const nsIXULAppInfo           = Components.interfaces.nsIXULAppInfo;

/**
 * 本体
 * unmht プロトコルからチャネルを生成する
 *   Inherits From: nsIProtocolHandler, arIUnMHTProtocolHandler
 */
function arUnMHTProtocolHandler () {
}
arUnMHTProtocolHandler.prototype = {
  scheme : "unmht", /* String  プロトコルスキーム */
  defaultPort : -1, /* Number  ポート番号
                     *   ネットワークベースではないので -1 */
    
  protocolFlags : nsIProtocolHandler.URI_NOAUTH, /* Number プロトコルの詳細
                                                  *   file と同様の
                                                  *   プロトコル */
    
  /**
   * インターフェースの要求
   *   nsISupports.QueryInterface
   *
   * @param  nsIIDRef iid
   *         インターフェース ID
   * @throws Components.results.NS_NOINTERFACE
   * @return nsIProtocolHandler/arIUnMHTProtocolHandler
   *         this
   */
  QueryInterface : function (iid) {
    if (iid.equals (nsISupports)
        || iid.equals (nsIProtocolHandler)
        || iid.equals (arIUnMHTProtocolHandler)) {
      return this;
    }
        
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
    
  /**
   * unmht:// 形式の URI にする
   *   arIUnMHTProtocolHandler.enUnMHTURI
   *
   * @param  String uri
   *         URI
   * @return String
   *         unmht:// 形式の URI
   */
  enUnMHTURI : function (uri) {
    if (uri
        .match (/^([A-Za-z0-9\-]+):(\/\/)?([^\/]*)(\/\/)?(\/)?(.*)$/)) {
      var protocol = RegExp.$1;
      var sep1 = RegExp.$2;
      var host = RegExp.$3;
      var sep2 = RegExp.$4;
      var sep3 = RegExp.$5;
      var path = RegExp.$6;
                 
      var sep = 0;
      if (sep1) {
        sep |= 1;
      }
      if (sep2) {
        sep |= 2;
      }
      if (sep3) {
        sep |= 4;
      }
            
      uri
      = "unmht://" + host + "/" + protocol + "." + sep + "/" + path + "/";
    }
        
    return uri;
  },
    
  /**
   * unmht:// 形式の URI かどうかを返す
   *   arIUnMHTProtocolHandler.isUnMHTURI
   *
   * @param  String uri
   *         URI
   * @return Boolean
   *         unmht:// 形式の URI かどうか
   */
  isUnMHTURI : function (uri) {
    var param = this.getUnMHTURIParam (uri);
    if ("original" in param) {
      return true;
    }
        
    return false;
  },
    
  /**
   * unmht:// 形式の URI を元に戻す
   *   arIUnMHTProtocolHandler.deUnMHTURI
   *
   * @param  String uri
   *         unmht:// 形式の URI
   * @return String
   *         URI
   */
  deUnMHTURI : function (uri) {
    var param = this.getUnMHTURIParam (uri);
    if ("original" in param) {
      return param.original;
    }
        
    return uri;
  },
    
  /**
   * unmht:// 形式の URI の情報を取得する
   *   arIUnMHTProtocolHandler.getUnMHTURIParam
   *
   * @param  String uri
   *         unmht:// 形式の URI
   * @return Object
   *         URI の情報
   */
  getUnMHTURIParam : function (uri) {
    var param = new Object ();
        
    if (uri.match
        (/^unmht:\/\/([^\/]*)\/([A-Za-z0-9\-]+)\.([0-9]+)\/(.*)\/([^\/]*)$/)) {
      param.host = RegExp.$1;
      param.protocol = RegExp.$2;
      var sep = parseInt (RegExp.$3);
      param.path = RegExp.$4;
      param.refName = RegExp.$5;
      
      if (param.refName.match (/^(.*)#(.*)$/)) {
        param.refName = RegExp.$1;
        param.fragment = RegExp.$2;
      }
            
      try {
        var idn
          = Components.classes
          ["@mozilla.org/network/idn-service;1"].
          getService (nsIIDNService);
                
        param.host = idn.convertUTF8toACE (param.host);
      }
      catch (e) {
      }
            
      var sep1 = (sep & 1) ? "//" : "";
      var sep2 = (sep & 2) ? "//" : "";
      var sep3 = (sep & 4) ? "/" : "";
            
      param.original
        = param.protocol + ":" + sep1 + param.host + sep2 + sep3
        + param.path;
            
      param.rootURI
        = "unmht://" + param.host + "/"
        + param.protocol + "." + sep + "/"
        + param.path + "/";
    }
        
    return param;
  },
    
  /**
   * ブラックリストのポートを上書きするか
   *   nsIProtocolHandler.allowPort
   *
   * @param  Number port
   *         ポート番号
   * @param  String scheme
   *         プロトコルスキーム
   * @return Boolean
   *         上書きするか
   */
  allowPort: function (port, scheme) {
    return false;
  },
    
  /**
   * URI を作成する
   *   nsIProtocolHandler.newURI
   *
   * @param  String spec
   *         対象の URI
   * @param  String charset
   *         対象の文字コード
   * @param  nsIURI baseURI
   *         読み込み元の URI
   * @return nsIURI
   *         作成した URI
   */
  newURI : function (spec, charset, baseURI) {
    var uri
    = Components.classes ["@mozilla.org/network/standard-url;1"]
    .createInstance (nsIURI);
    if (spec.match (/^[A-Za-z0-9\-]+:\/\//)) {
      /* フルの URI */
      uri.spec = spec;
    }
    else if (baseURI) {
      var entries
      = Components.classes ["@mozilla.org/appshell/window-mediator;1"]
      .getService (Components.interfaces.nsIWindowMediator)
      .getEnumerator ("navigator:browser");
      
      while (entries.hasMoreElements ()) {
        var w = entries.getNext ();
        
        var file = w.UnMHT.getFile (baseURI);
        if (file) {
          uri.spec = w.UnMHTFileManager.jointPath (file.location, spec);
          return uri;
        }
      }
      
      uri.spec = baseURI.resolve (spec);
    }
    else {
      uri.spec = spec;
    }
        
    return uri;
  },
    
  /**
   * チャネルを作成する
   *   nsIProtocolHandler.newChannel
   *
   * @param  nsIURI uri
   *         対象の URI
   * @return nsIChannel
   *         作成したチャネル
   */
  newChannel : function (uri) {
    try {
      var param = this.getUnMHTURIParam (uri.spec);
      if (param.protocol == "cache") {
        /* キャッシュ情報 */
        return this._createCacheChannel (uri);
      }
      else if (param.protocol == "cid"
               || param.protocol == "dummy") {
        /* CID、ローカルファイル */
        return this._createEmptyChannel (uri);
      }
      else if (param.protocol == "error") {
        /* エラー情報 */
        return this._createErrorChannel (uri);
      }
      else {
        /* mht 内のファイル */
        return this._createFileChannel (uri);
      }
    }
    catch (e) {
    }
        
    return this._createEmptyChannel (uri);
  },
    
  /**
   * mht 内のファイルのチャネルを作成する
   *
   * @param  nsIURI uri
   *         対象の URI (umht:// 形式)
   * @return nsIChannel
   *         作成したチャネル
   */
  _createFileChannel : function (uri) {
    var entries
    = Components.classes ["@mozilla.org/appshell/window-mediator;1"]
    .getService (Components.interfaces.nsIWindowMediator)
    .getEnumerator ("navigator:browser");
        
    while (entries.hasMoreElements ()) {
      var w = entries.getNext ();
      
      var file = w.UnMHT.getFile (uri);
      if (file) {
        if (w.UnMHT.memoryCacheMode) {
          var pipe
            = Components.classes ["@mozilla.org/pipe;1"]
            .createInstance (nsIPipe);
                    
          pipe.init (false, false, file.content.length, 1, null);
                    
          pipe.outputStream.write (file.content, file.content.length);
          pipe.outputStream.close ();
                    
          var inputStreamChannel
            = Components
            .classes ["@mozilla.org/network/input-stream-channel;1"]
            .createInstance (nsIInputStreamChannel);
          
          inputStreamChannel.setURI (uri);
          inputStreamChannel.QueryInterface (nsIChannel).contentType
            = file.filetype;
          inputStreamChannel.QueryInterface (nsIChannel)
            .contentCharset = file.charset;
          inputStreamChannel.contentStream = pipe.inputStream;
          inputStreamChannel.QueryInterface (nsIChannel)
            .contentLength = file.content.length;
                    
          return inputStreamChannel;
        }
        else {
          var localfile
            = Components.classes ["@mozilla.org/file/local;1"]
            .createInstance (nsILocalFile);
          localfile.initWithPath (file.nativeFilename);
          if (localfile.exists ()) {
            var fstream
              = Components.classes
              ["@mozilla.org/network/file-input-stream;1"]
              .createInstance (nsIFileInputStream);
            fstream.init (localfile, 0x01, 0444, 0);
                        
            var inputStreamChannel
              = Components.classes
              ["@mozilla.org/network/input-stream-channel;1"]
              .createInstance (nsIInputStreamChannel);
                        
            inputStreamChannel.setURI (uri);
            inputStreamChannel.QueryInterface (nsIChannel)
              .contentType = file.filetype;
            inputStreamChannel.contentStream = fstream;
            inputStreamChannel.QueryInterface (nsIChannel)
              .contentLength = localfile.fileSize;
                    
            return inputStreamChannel;
          }
        }
      }
    }
    
    /* 見付からないので一旦空のチャネルを返す */
    return this._createEmptyChannel (uri);
  },
    
  /**
   * キャッシュ情報のチャネルを作成する
   *
   * @param  nsIURI uri
   *         対象の URI
   * @return nsIChannel
   *         作成したチャネル
   */
  _createCacheChannel : function (uri) {
    var head, foot, data;
        
    var body = "";
    var size = 0;
    var entry = 0;
    var aliveWindow = null;
        
    try {
      var entries
        = Components
        .classes ["@mozilla.org/appshell/window-mediator;1"]
        .getService (Components.interfaces.nsIWindowMediator)
        .getEnumerator ("navigator:browser");
            
      var i = 0;
      while (entries.hasMoreElements ()) {
        var w = entries.getNext ();
        try {
          var info = w.UnMHT.getCacheInfo ();
          aliveWindow = w;
          if (body != "" && info [0] != "") {
            body += "<hr />";
          }
          body += info [0];
          size += info [1];
          entry += info [2];
          i ++;
        }
        catch (e) {
        }
      }
    }
    catch (e) {
    }
        
    if (aliveWindow) {
      head = aliveWindow.UnMHT.getCacheHead (entry, size);
      foot = aliveWindow.UnMHT.getCacheFoot ();
    }
    else {
      head = "";
      foot = "";
    }
        
    data = head + body + foot;
        
    var sstream
    = Components.classes ["@mozilla.org/io/string-input-stream;1"]
    .createInstance (nsIStringInputStream);
    sstream.setData (data, data.length);
        
    var inputStreamChannel
    = Components.classes ["@mozilla.org/network/input-stream-channel;1"]
    .createInstance (nsIInputStreamChannel);
        
    inputStreamChannel.setURI (uri);
    inputStreamChannel.QueryInterface (nsIChannel).contentCharset
    = "utf-8";
    inputStreamChannel.QueryInterface (nsIChannel).contentType
    = "text/html";
    inputStreamChannel.contentStream = sstream;
    inputStreamChannel.QueryInterface (nsIChannel).contentLength
    = data.length;
        
    return inputStreamChannel;
  },
    
  /**
   * エラー情報のチャネルを作成する
   *
   * @param  nsIURI uri
   *         対象の URI
   * @return nsIChannel
   *         作成したチャネル
   */
  _createErrorChannel : function (uri) {
    var location = this.deUnMHTURI (uri.spec);
    location = location.replace (/^error:/, "unmht:");
    location = this.deUnMHTURI (location);
        
    var aliveWindow = null;
    var data = "";
        
    try {
      var entries
        = Components
        .classes ["@mozilla.org/appshell/window-mediator;1"]
        .getService (Components.interfaces.nsIWindowMediator)
        .getEnumerator ("navigator:browser");
            
      var i = 0;
      while (entries.hasMoreElements ()) {
        var w = entries.getNext ();
        try {
          data = w.UnMHT.getErrorPage (location);
        }
        catch (e) {
        }
      }
    }
    catch (e) {
    }
        
    var sstream
    = Components.classes ["@mozilla.org/io/string-input-stream;1"]
    .createInstance (nsIStringInputStream);
    sstream.setData (data, data.length);
        
    var inputStreamChannel
    = Components.classes ["@mozilla.org/network/input-stream-channel;1"]
    .createInstance (nsIInputStreamChannel);
        
    inputStreamChannel.setURI (uri);
    inputStreamChannel.QueryInterface (nsIChannel).contentCharset
    = "utf-8";
    inputStreamChannel.QueryInterface (nsIChannel).contentType
    = "text/html";
    inputStreamChannel.contentStream = sstream;
    inputStreamChannel.QueryInterface (nsIChannel).contentLength
    = data.length;
        
    return inputStreamChannel;
  },
  /**
   * 空のチャネルを作成する
   *
   * @param  nsIURI uri
   *         対象の URI
   * @return nsIChannel
   *         作成したチャネル
   */
  _createEmptyChannel : function (uri) {
    var sstream
    = Components.classes ["@mozilla.org/io/string-input-stream;1"]
    .createInstance (nsIStringInputStream);
    sstream.setData ("", 0);
        
    var inputStreamChannel
    = Components.classes ["@mozilla.org/network/input-stream-channel;1"]
    .createInstance (nsIInputStreamChannel);
        
    inputStreamChannel.setURI (uri);
    inputStreamChannel.QueryInterface (nsIChannel).contentCharset
    = "utf-8";
    inputStreamChannel.QueryInterface (nsIChannel).contentType
    = "text/html";
    inputStreamChannel.contentStream = sstream;
    inputStreamChannel.QueryInterface (nsIChannel).contentLength
    = 0;
        
    return inputStreamChannel;
  }
};

/**
 * 本体のファクトリー
 *   Inherits From: nsIFactory
 */
var arUnMHTProtocolHandlerFactory = {
  /**
   * 本体を生成する
   *   nsIFactory.createInstance
   *
   * @param  nsISupport outer
   *          統合する対象
   * @param  nsIIDRef iid
   *         生成する対象のインターフェース ID
   * @return arUnMHTProtocolHandler
   *         本体
   */
  createInstance : function (outer, iid) {
    if (outer != null) {
      /* 統合する対象がある場合はエラー */
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    }
        
    var handler;
    handler = new arUnMHTProtocolHandler ();
    if ("URI_LOADABLE_BY_ANYONE" in nsIProtocolHandler) {
      handler.protocolFlags |= nsIProtocolHandler.URI_LOADABLE_BY_ANYONE;
    }
        
    return handler.QueryInterface (iid);
  }
};

/**
 * XPCOM のモジュール
 *   Inherits From: nsIModule
 */
var arUnMHTProtocolHandlerModule = {
  /* 本体に関する情報 */
  CONTRACTID: "@mozilla.org/network/protocol;1?name=unmht",
  CID: Components.ID ("{d605b959-0688-4846-887c-f6658725f7f1}"),
  CNAME: "UnMHT Protocol Handler JS Component",
    
  /**
   * インターフェースの要求
   *   nsISupports.QueryInterface
   *
   * @param  nsIIDRef iid
   *         インターフェースID
   * @throws Components.results.NS_NOINTERFACE
   * @return nsIModule
   *         this
   */
  QueryInterface : function (iid) {
    if (iid.equals (nsISupports)
        || iid.equals (nsIModule)) {
      return this;
    }
        
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
    
  /**
   * 登録処理
   *   nsIModule.registerSelf
   *
   * @param  nsIComponentManager compMgr
   * @param  nsIFile fileSpec
   *         モジュールのファイル
   *           reference と他のソースコード中で並びが違う
   * @param  String location
   *         不明
   *           reference と他のソースコード中で並びが違う
   * @param  String type
   *         ローダの種類
   */
  registerSelf : function (compMgr, fileSpec, location, type) {
    try {
      var id
      = Components.classes ["@mozilla.org/xre/app-info;1"]
      .getService (nsIXULAppInfo).ID;
      if (id == "{3550f703-e582-4d05-9a08-453d09bdfdc6}") {
        /* Thunderbird */
            
        return;
      }
    }
    catch (e) {}
        
    if ("URI_LOADABLE_BY_ANYONE" in nsIProtocolHandler) {
      arUnMHTProtocolHandler.prototype.protocolFlags
      |= nsIProtocolHandler.URI_LOADABLE_BY_ANYONE;
    }
        
    compMgr = compMgr.QueryInterface (nsIComponentRegistrar);
    compMgr.registerFactoryLocation (this.CID,
                                     this.CNAME,
                                     this.CONTRACTID,
                                     fileSpec, location, type);
  },
    
  /**
   * 登録解除処理
   *   nsIModule.unregisterSelf
   *
   * @param  nsIComponentManager compMgr
   * @param  nsIFile fileSpec
   *         モジュールのファイル
   *           reference と他のソースコード中で並びが違う
   * @param  String location
   *         不明
   *           reference と他のソースコード中で並びが違う
   */
  unregisterSelf : function (compMgr, fileSpec, location) {
    try {
      var id
      = Components.classes ["@mozilla.org/xre/app-info;1"]
      .getService (nsIXULAppInfo).ID;
      if (id == "{3550f703-e582-4d05-9a08-453d09bdfdc6}") {
        /* Thunderbird */
            
        return;
      }
    } catch (e) {}
        
    compMgr = compMgr.QueryInterface (nsIComponentRegistrar);
    compMgr.unregisterFactoryLocation (this.CID, fileSpec);
  },
    
  /**
   * ファクトリーオブジェクトを取得する
   *   nsIModule.getClassObject
   *
   * @param  nsIComponentManager compMgr
   * @param  nsCIDRef cid
   *         取得対象のクラス ID
   * @param  nsIIDRef iid
   *         取得対象のインターフェース ID
   * @throws Components.results.NS_ERROR_NOT_IMPLEMENTED
   *         Components.results.NS_ERROR_NO_INTERFACE
   * @return arUnMHTProtocolHandlerFactory
   *         本体のファクトリー
   */
  getClassObject : function (compMgr, cid, iid) {
    if (cid.equals (this.CID)) {
      return arUnMHTProtocolHandlerFactory;
    }
        
    if (!iid.equals (nsIFactory)) {
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    }
        
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
    
  /**
   * 終了できるかどうか
   *   nsIModule.canUnload
   *
   * @param  nsIComponentManager compMgr
   * @return Boolean
   *         終了できるかどうか
   */
  canUnload : function (compMgr) {
    return true;
  }
};

/**
 * モジュールを取得する
 * @param  nsIComponentManager compMgr
 * @param  nsIFile fileSpec
 *         モジュールのファイル
 * @return arUnMHTProtocolHandlerModule
 *         モジュール
 */
function NSGetModule (compMgr, fileSpec) {
  return arUnMHTProtocolHandlerModule;
}
