'use strict';

var fs       = require('fs'),
    path     = require('path'),
    defaults = require('lodash.defaults');

var PACKAGE_NAME = require('../package.json').name;

/**
 * Factory for find-file with the given <code>options</code> hash.
 * @param {{debug: boolean, attempts:number}} [opt] Optional options hash
 */
function findFile(opt) {
  var options = defaults(opt, {
    debug: false,
    attempts: 0
  });
  return {
    absolute: absolute,
    base    : base
  };

  /**
   * Search for the relative file reference from the <code>startPath</code> up to the process
   * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
   * @param {string} startPath The location of the uri declaration and the place to start the search from
   * @param {string} uri The content of the url() statement, expected to be a relative file path
   * @param {string} [limit] Optional directory to limit the search to
   * @returns {string|null} <code>null</code> where not found else the absolute path to the file
   */
  function absolute(startPath, uri, limit) {
    var basePath = base(startPath, uri, limit);
    return !!basePath && path.resolve(basePath, uri) || null;
  }

  /**
   * Search for the relative file reference from the <code>startPath</code> up to the process
   * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
   * @param {string} startPath The location of the uri declaration and the place to start the search from
   * @param {string} uri The content of the url() statement, expected to be a relative file path
   * @param {string} [limit] Optional directory to limit the search to
   * @returns {string|null} <code>null</code> where not found else the base path upon which the uri may be resolved
   */
  function base(startPath, uri, limit) {
    var messages = [];

    // ensure we have some limit to the search
    limit = limit && path.resolve(limit) || process.cwd();

    // #69 limit searching: make at least one attempt
    var remaining = Math.max(0, options.attempts) || 1E+9;

    // ignore explicit uris data|http|https and ensure we are at a valid start path
    var absoluteStart = !(/^(data|https?):/.test(uri)) && path.resolve(startPath);
    if (absoluteStart) {

      // find path to the root, stopping at cwd, package.json or bower.json
      var pathToRoot = [];
      var isWorking;
      do {
        pathToRoot.push(absoluteStart);
        isWorking = testWithinLimit(absoluteStart) && testNotPackage(absoluteStart);
        absoluteStart = path.resolve(absoluteStart, '..');
      } while (isWorking);

      // start a queue with the path to the root
      var appendLimit = options.includeRoot && pathToRoot.indexOf(limit) === -1 ? limit : [];
      var queue = pathToRoot.concat(appendLimit);

      // the queue pattern ensures that we favour paths closest the the start path
      // process the queue until empty or until we exhaust our attempts
      while (queue.length && (remaining-- > 0)) {

        // shift the first item off the queue, consider it the base for our relative uri
        var basePath = queue.shift();
        var fullPath = path.resolve(basePath, uri);
        messages.push(basePath);

        // file exists so convert to a dataURI and end
        if (fs.existsSync(fullPath)) {
          flushMessages('FOUND');
          return basePath;
        }
        // enqueue subdirectories that are not packages and are not in the root path
        else {
          enqueue(queue, basePath);
        }
      }

      // interrupted by options.attempts
      if (queue.length) {
        flushMessages('NOT FOUND (INTERRUPTED)');
      }
      // not found
      else {
        flushMessages('NOT FOUND');
        return null;
      }
    }
    // ignored
    else {
      flushMessages('IGNORED');
      return null;
    }

    /**
     * Enqueue subdirectories that are not packages and are not in the root path
     * @param {Array} queue The queue to add to
     * @param {string} basePath The path to consider
     */
    function enqueue(queue, basePath) {
      fs.readdirSync(basePath)
        .filter(function notHidden(filename) {
          return (filename.charAt(0) !== '.');
        })
        .map(function toAbsolute(filename) {
          return path.join(basePath, filename);
        })
        .filter(function directoriesOnly(absolutePath) {
          return fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory();
        })
        .filter(function notInRootPath(absolutePath) {
          return (pathToRoot.indexOf(absolutePath) < 0);
        })
        .filter(testNotPackage)
        .forEach(function enqueue(absolutePath) {
          queue.push(absolutePath);
        });
    }

    /**
     * Test whether the given directory is above but not equal to any of the project root directories.
     * @param {string} absolutePath An absolute path
     * @returns {boolean} True where a package.json or bower.json exists, else False
     */
    function testWithinLimit(absolutePath) {
      var relative = path.relative(limit, absolutePath);
      return !!relative && (relative.slice(0, 2) !== '..');
    }

    /**
     * Print verbose debug info where <code>options.debug</code> is in effect.
     * @param {string} result Final text to append to the message
     */
    function flushMessages(result) {
      if (options.debug) {
        var text = ['\n' + PACKAGE_NAME + ': ' + uri]
          .concat(messages)
          .concat(result)
          .join('\n  ');
        console.log(text);
      }
    }
  }

  /**
   * Test whether the given directory is the root of its own package.
   * @param {string} absolutePath An absolute path
   * @returns {boolean} True where a package.json or bower.json exists, else False
   */
  function testNotPackage(absolutePath) {
    return ['package.json', 'bower.json'].every(function fileFound(file) {
      return !fs.existsSync(path.resolve(absolutePath, file));
    });
  }
}

module.exports = findFile;
