/* ***** 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 Verbosio File Search Components.
 *
 * 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://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://app/modules/ArrayConverter.jsm");
Components.utils.import("resource://app/modules/ecma-debug.jsm");

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

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

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

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

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

function NULL_POINTER() {
  throw Components.results.NS_ERROR_NULL_POINTER;
}

function INVALID_POINTER() {
  throw Components.results.NS_ERROR_INVALID_POINTER;
}

/**
 * Wrap a JavaScript object for passing to components code.
 */
function ObjectWrapper(object) {
  this.wrappedJSObject = object;
}

const C_i = Components.interfaces;

const xeIFileSearchEngine         = C_i.xeIFileSearchEngine;
const xeIFileSearchResult         = C_i.xeIFileSearchResult;
const mozIJSSubScriptLoader       = C_i.mozIJSSubScriptLoader;
const nsIScriptError              = C_i.nsIScriptError;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsIComponentRegistrar       = C_i.nsIComponentRegistrar;
const nsIFactory                  = C_i.nsIFactory;
const nsIModule                   = C_i.nsIModule;
const nsISupports                 = C_i.nsISupports;

var subscriptsLoaded = false;
/**
 * Load necessary scripts.
 */
function loadSubscripts() {
  if (subscriptsLoaded) {
    return;
  }
  var jsLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                           .getService(mozIJSSubScriptLoader);
  jsLoader.loadSubScript("chrome://jslib/content/jslib.js");
  jsLoader.loadSubScript("chrome://jslib/content/io/io.js");
  subscriptsLoaded = true;
}

/*****************************************************************************
 * File Search Engine                                                        *
 *****************************************************************************/

function FileSearchEngine() {
  loadSubscripts();
  this._readyToRun = false;
  this.searchCriteria = [];
}
FileSearchEngine.prototype = {
  /**
   * Search a directory for files and directories which match one criteria set.
   *
   * @param aBaseDir   JSLib Dir object for the directory.
   * @param aRecursive If true, search sub-directories as well.
   *
   * @return JSArray of xeIFileSearchResult objects.
   */
  searchFiles: function searchFiles(aBaseDir, aRecursive) {
    this._readyToRun = false;
    assert(aBaseDir instanceof Dir, "Not a directory!");
    assert(aBaseDir.isDir(), "Not a directory!");

    var files = aBaseDir.readDir();
    var results = [];
    for (var i = 0; i < files.length; i++) {
      var pass = (this.searchCriteria.length == 0) ?
                 new FileSearchResult(files[i].path) :
                 null;
      var currentFile = files[i];
      var criteriaIndex = -1;
      for (var j = 0;
            (j < this.searchCriteria.length) && !pass;
            j++) {
        var criteria = this.searchCriteria[j];

        try {
          this._readyToRun = true;
          pass = criteria.runTests(currentFile);
        } catch (e) {
          this._readyToRun = false;
          throw e;
        }
        this._readyToRun = false;

        if (pass) {
          criteriaIndex = j;
        }
      }

      if (pass) {
        assert(pass.QueryInterface(xeIFileSearchResult), "Where'd it come from?");
        try {
          this._readyToRun = true;
          pass.setCriteriaIndex(criteriaIndex);
        } catch (e) {
          this._readyToRun = false;
          throw e;
        }
        this._readyToRun = false;
        results.push(pass);
      }

      if (aRecursive && currentFile.isDir()) {
        results = results.concat(this.searchFiles(currentFile, true));
      }
    }

    return results;
  },

  // xeIFileSearchEngine
  reset: function reset() {
    this.searchCriteria = [];
  },

  // xeIFileSearchEngine
  getNewCriteria:  function getNewCriteria() {
    var criteria = new FileSearchCriteria(this);
    this.searchCriteria[this.searchCriteria.length] = criteria;
    return criteria;
  },

  // xeIFileSearchEngine
  searchDirectory: function searchDirectory(aLocalPath, aRecursive) {
    for (var i = 0; i < this.searchCriteria.length; i++) {
      var criteria = this.searchCriteria[i];
      assert(criteria.QueryInterface(xeIFileSearchCriteria), i + " not valid!");
    }

    var baseDir = new Dir(aLocalPath);
    var results = this.searchFiles(baseDir, aRecursive);
    return ArrayConverter.enumerator(results);
  },

  _interfaces: [xeIFileSearchEngine,
                nsIClassInfo],

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

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

  // nsIClassInfo
  classDescription: "File search engine",

  // nsIClassInfo
  classID: Components.ID("0dca4b97-60d0-4017-97e5-1075d7814900"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/file-search/engine;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0,

  // 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;

    // 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: FileSearchEngine " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: FileSearchEngine Unknown\n");
    }

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

/*****************************************************************************
 * File Search Criteria                                                      *
 *****************************************************************************/
const xeIFileSearchCriteria = C_i.xeIFileSearchCriteria;

function FileSearchCriteria(aEngine) {
  this._ownerEngine = aEngine;
  this.propertyStringFilters   = [];
  this.propertyFunctionFilters = [];
  this.contentStringFilters    = [];
  this.contentFunctionFilters  = [];
}
FileSearchCriteria.prototype = {
  /**
   * Run tests on a file.
   *
   * @param currentFile The JS file to run tests on.
   *
   * @return xeIFIleSearchResult if the tests pass, null otherwise.
   */
  runTests: function runTests(currentFile) {
    if (!this._ownerEngine._readyToRun) {
      ERROR_ABORT();
    }
    var filter;
    var filterResult;
    var i;

    // Property string filters
    for (i = 0; i < this.propertyStringFilters.length; i++) {
      filter = this.propertyStringFilters[i];
      switch (typeof currentFile[filter.name]) {
        case "undefined":
          return null;
        case "function":
          filterResult = currentFile[filter.name]().toString();
          break;
        default:
          filterResult = currentFile[filter.name].toString();
          break;
      }

      if (filterResult != filter.value) {
        return null;
      }
    }

    // Property function filters
    for (i = 0; i < this.propertyFunctionFilters.length; i++) {
      filter = this.propertyFunctionFilters[i];
      switch (typeof currentFile[filter.name]) {
        case "undefined":
          return null;
        case "function":
          filterResult = currentFile[filter.name]().toString();
          break;
        default:
          filterResult = currentFile[filter.name].toString();
          break;
      }

      if (!filter.method.test(filterResult)) {
        return null;
      }
    }

    // Run content filters last!  File reading is more expensive.
    if (this.contentStringFilters.length + this.contentFunctionFilters.length > 0) {
      if (!currentFile.isFile()) {
        return null;
      }
      currentFile.open("r");
      var contents = currentFile.read();
      result._fileContents = contents;
      currentFile.close();

      for (i = 0; i < this.contentStringFilters.length; i++) {
        filter = this.contentStringFilters[i];
        filterResult = contents.indexOf(filter);
        if (filterResult == -1) {
          return null;
        }

        // Advance to next line break.
        var nextBreak = contents.indexOf("\n", filterResult);
        var data = contents.substr(0, nextBreak).split("\n");
        result.setLine(data.length - 1, data[data.length - 1]);
      }

      for (i = 0; i < this.contentFunctionFilters.length; i++) {
        filter = this.contentFunctionFilters[i];
        assert(filter instanceof Components.interfaces.xeIFileSearchFilter, "Not a filter?")
        dump(filter + "\n");

        match = filter.test(contents);
        dump("content filter match: " + match + "\n");
        if (!match) {
          return null;
        }

        // Advance to next line break.
        var startIndex = contents.indexOf(match) + match.length;
        var nextBreak = contents.indexOf("\n", startIndex);
        var data = contents.substr(0, nextBreak).split("\n");
        result.setLine(data.length - 1, data[data.length - 1]);
      }
    }

    // If a file gets this far, it has passed all filtering.
    return new FileSearchResult(currentFile.path, this._ownerEngine);
  },

  // xeIFileSearchCriteria
  filterPropertyString: function filterPropertyString(aName, aValue) {
    if (!aName) {
      NULL_POINTER();
    }

    this.propertyStringFilters.push({
      name: aName,
      value: aValue
    });
  },

  // xeIFileSearchCriteria
  filterPropertyFunction: function filterPropertyFunction(aName, aFilter) {
    if (!aName || !aFilter) {
      NULL_POINTER();
    }

    this.propertyFunctionFilters.push({
      name: aName,
      method: aFilter
    });
  },

  // xeIFileSearchCriteria
  filterContentsString: function filterContentsString(aRequiredContent) {
    if (!aRequiredContent) {
      NULL_POINTER();
    }

    this.contentStringFilters.push(aRequiredContent);
  },

  // xeIFileSearchCriteria
  filterContentsFunction: function filterContentsFunction(aFilter) {
    if (!aFilter) {
      NULL_POINTER();
    }

    var filter = aFilter.QueryInterface(xeIFileSearchResult);
    this.contentFunctionFilters.push(filter);
  },

  _interfaces: [xeIFileSearchCriteria,
                nsIClassInfo],

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

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

  // nsIClassInfo
  classDescription: null,

  // nsIClassInfo
  classID: null,

  // nsIClassInfo
  contractID: null,

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0,

  // 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: FileSearchCriteria " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: FileSearchCriteria Unknown\n");
    }

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

/*****************************************************************************
 * File Search Result                                                        *
 *****************************************************************************/

/**
 * Construct a xeIFileSearchResult object.
 *
 * @param aPath   Path to the file.
 * @param aEngine The search engine owning this result.
 */
function FileSearchResult(aPath, aEngine)
{
  this._ownerEngine = aEngine;

  // xeIFileSearchResult
  this.path = aPath;

  // xeIFileSearchResult
  this.criteriaIndex = -1;

  this._lineNumbers = [];
  this._fileContents = null;
  this._lines = {};
}
FileSearchResult.prototype =
{
  // xeIFileSearchResult
  getLineNumbers: ArrayConverter.xpcomArrayMethod("_lineNumbers"),

  // xeIFileSearchResult
  getLine: function getLine(aIndex) {
    if (!(aIndex in this._lines)) {
      INVALID_POINTER();
    }
    return this._lines[aIndex];
  },

  // xeIFileSearchResult
  setLine: function setLine(aIndex, aLine) {
    if (!this._ownerEngine._readyToRun) {
      ERROR_ABORT();
    }
    dump("Setting line " + aIndex + " to " + aLine + "\n");
    this._lineNumbers[this._lineNumbers.length] = aIndex;
    this._lines[aIndex] = aLine;
  },

  // xeIFileSearchResult
  setCriteriaIndex: function setCriteriaIndex(aIndex) {
    if (!this._ownerEngine._readyToRun) {
      ERROR_ABORT();
    }

    this.criteriaIndex = aIndex;
  },

  _interfaces: [xeIFileSearchResult,
                nsIClassInfo],

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

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

  // nsIClassInfo
  classDescription: null,

  // nsIClassInfo
  classID: null,

  // nsIClassInfo
  contractID: null,

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0,

  // 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: FileSearchResult " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE: FileSearchResult Unknown\n");
    }

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

var NSGetModule = XPCOMUtils.generateNSGetModule([
  FileSearchEngine
], null, null);

/*****************************************************************************
 * Sample Test Code For Searching                                            *
 *****************************************************************************/
/*
function runSearch(aLocalPath) {
  const xeIFileSearchEngine   = Components.interfaces.xeIFileSearchEngine;
  const xeIFileSearchCriteria = Components.interfaces.xeIFileSearchCriteria;
  const xeIFileSearchResult   = Components.interfaces.xeIFileSearchResult;

  const fileSearch = Components.classes["@verbosio.mozdev.org/file-search/engine;1"]
                               .createInstance(xeIFileSearchEngine);
  fileSearch.reset();

  var criteria = fileSearch.getNewCriteria();
  criteria.filterPropertyString("isFile", "true");
  criteria.filterPropertyString("leaf", "chrome.manifest");

  criteria.filterContentsString("content xml-inspector");
  criteria.filterContentsString("locale xml-inspector");
  criteria.filterContentsFunction({
    test: function test(aValue) {
      var re = /skin\s+xml-inspector\s+/;
      var search = aValue.match(re);
      var match;
      if (search) {
        match = search[0];
      } else {
        match = null;
      }

      return match;
    },

    QueryInterface: function QueryInterface(aIID) {
      if (aIID.equals(xeIFileSearchFilter) ||
          aIID.equals(nsISupports)) {
        return this;
      }

      throw Components.results.NS_ERROR_NO_INTERFACE;
    }
  });

  // nsISimpleEnumerator
  var enumerator = fileSearch.searchDirectory(aLocalPath, true);

  while (enumerator.hasMoreElements()) {
    var result = enumerator.getNext();
    if (!(result instanceof xeIFileSearchResult)) {
      throw new Error("Not a search result?");
    }
    dump(result.path + "(" + result.criteriaIndex + ")\n");
    var countObject = {};
    var lineNumbers = result.getLineNumbers(countObject);
    for (var i = 0; i < countObject.value; i++) {
      dump("  " + lineNumbers[i] + ":" + result.getLine(lineNumbers[i]) + "\n");
    }
    dump("***\n\n");
  }
}
 */
