/*
 * Jake JavaScript build tool
 * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

var EventEmitter = require('events').EventEmitter;
// And so it begins
global.jake = new EventEmitter();

var fs = require('fs')
  , path = require('path')
  , taskNs = require('./task')
  , Task = taskNs.Task
  , FileTask = taskNs.FileTask
  , DirectoryTask = taskNs.DirectoryTask
  , Rule = require('./rule').Rule
  , Namespace = require('./namespace').Namespace
  , api = require('./api')
  , utils = require('./utils')
  , Program = require('./program').Program
  , Loader = require('./loader').Loader
  , pkg = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString());

var MAX_RULE_RECURSION_LEVEL = 16;

var Invocation = function (taskName, args) {
  this.taskName = taskName;
  this.args = args;
};

// Globalize jake and top-level API methods (e.g., `task`, `desc`)
utils.mixin(global, api);

// Copy utils onto base jake
utils.mixin(jake, utils);
// File utils should be aliased directly on base jake as well
utils.mixin(jake, utils.file);

utils.mixin(jake, new (function () {

  this._invocationChain = [];

  // Private variables
  // =================
  // Local reference for scopage
  var self = this;

  // Public properties
  // =================
  this.version = pkg.version;
  // Used when Jake exits with a specific error-code
  this.errorCode = undefined;
  // Loads Jakefiles/jakelibdirs
  this.loader = new Loader();
  // Name/value map of all the various tasks defined in a Jakefile.
  // Non-namespaced tasks are placed into 'default.'
  this.defaultNamespace = new Namespace('default', null);
  // For namespaced tasks -- tasks with no namespace are put into the
  // 'default' namespace so lookup code can work the same for both
  // namespaced and non-namespaced.
  this.currentNamespace = this.defaultNamespace;
  // Saves the description created by a 'desc' call that prefaces a
  // 'task' call that defines a task.
  this.currentTaskDescription = null;
  this.program = new Program();
  this.FileList = require('./file_list').FileList;
  this.PackageTask = require('./package_task').PackageTask;
  this.NpmPublishTask = require('./npm_publish_task').NpmPublishTask;
  this.WatchTask = require('./watch_task').WatchTask;
  this.TestTask = require('./test_task').TestTask;
  this.Task = Task;
  this.FileTask = FileTask;
  this.DirectoryTask = DirectoryTask;
  this.Namespace = Namespace;
  this.Rule = Rule;

  this.parseAllTasks = function () {
    var _parseNs = function (name, ns) {
      var nsTasks = ns.tasks
        , task
        , nsNamespaces = ns.childNamespaces
        , fullName;
      // Iterate through the tasks in each namespace
      for (var q in nsTasks) {
        task = nsTasks[q];
        // Prefix namespaced tasks
        fullName = name == 'default' ? q : name + ':' + q;
        // Save with 'taskname' or 'namespace:taskname' key
        task.fullName = fullName;
        jake.Task[fullName] = task;
      }
      for (var p in nsNamespaces) {
        fullName = (name == 'default') ? p : name + ':' + p;
        _parseNs(fullName, nsNamespaces[p]);
      }
    };

    _parseNs('default', jake.defaultNamespace);
  };

  /**
   * Displays the list of descriptions avaliable for tasks defined in
   * a Jakefile
   */
  this.showAllTaskDescriptions = function (f) {
    var p
      , maxTaskNameLength = 0
      , task
      , str = ''
      , padding
      , name
      , descr
      , filter = typeof f == 'string' ? f : null;

    for (p in jake.Task) {
      task = jake.Task[p];
      // Record the length of the longest task name -- used for
      // pretty alignment of the task descriptions
      maxTaskNameLength = p.length > maxTaskNameLength ?
        p.length : maxTaskNameLength;
    }
    // Print out each entry with descriptions neatly aligned
    for (p in jake.Task) {
      if (filter && p.indexOf(filter) == -1) {
        continue;
      }
      task = jake.Task[p];

      name = '\033[32m' + p + '\033[39m ';

      // Create padding-string with calculated length
      padding = (new Array(maxTaskNameLength - p.length + 2)).join(' ');

      descr = task.description;
      if (descr) {
        descr = '\033[90m # ' + descr + '\033[39m \033[37m \033[39m';
        console.log('jake ' + name + padding + descr);
      }
    }
  };

  this.createTask = function () {
    var args = Array.prototype.slice.call(arguments)
      , arg
      , obj
      , task
      , type
      , name
      , action
      , opts = {}
      , prereqs = [];

      type = args.shift();

    // name, [deps], [action]
    // Name (string) + deps (array) format
    if (typeof args[0] == 'string') {
      name = args.shift();
      if (Array.isArray(args[0])) {
        prereqs = args.shift();
      }
    }
    // name:deps, [action]
    // Legacy object-literal syntax, e.g.: {'name': ['depA', 'depB']}
    else {
      obj = args.shift();
      for (var p in obj) {
        prereqs = prereqs.concat(obj[p]);
        name = p;
      }
    }

    // Optional opts/callback or callback/opts
    while ((arg = args.shift())) {
      if (typeof arg == 'function') {
        action = arg;
      }
      else {
        opts = arg;
      }
    }

    task = jake.currentNamespace.resolveTask(name);
    if (task && !action) {
      // Task already exists and no action, just update prereqs, and return it.
      task.prereqs = task.prereqs.concat(prereqs);
      return task;
    }

    switch (type) {
      case 'directory':
        action = function () {
          jake.mkdirP(name);
        };
        task = new DirectoryTask(name, prereqs, action, opts);
        break;
      case 'file':
        task = new FileTask(name, prereqs, action, opts);
        break;
      default:
        task = new Task(name, prereqs, action, opts);
    }

    if (jake.currentTaskDescription) {
      task.description = jake.currentTaskDescription;
      jake.currentTaskDescription = null;
    }
    jake.currentNamespace.tasks[name] = task;
    task.namespace = jake.currentNamespace;

    // FIXME: Should only need to add a new entry for the current
    // task-definition, not reparse the entire structure
    jake.parseAllTasks();

    return task;
  };

  this.attemptRule = function (name, ns, level) {
    var prereqRule
      , prereq;
    if (level > MAX_RULE_RECURSION_LEVEL) {
      return null;
    }
    // Check Rule
    prereqRule = ns.matchRule(name);
    if (prereqRule) {
      prereq = prereqRule.createTask(name, level);
    }
    return prereq || null;
  };

  this.createPlaceholderFileTask = function (name, namespace) {
    var nsPath = ''
      , filePath = name.split(':').pop() // Strip any namespace
      , parts
      , fileTaskName
      , task
      , stats;

    if (namespace) {
      if (typeof namespace == 'string') {
        nsPath = namespace;
      }
      else {
        nsPath = namespace.path;
      }
    }

    parts = nsPath.length ? nsPath.split(':') : [];
    parts.push(filePath);
    fileTaskName = parts.join(':');

    task = jake.Task[fileTaskName];

    // If there's not already an existing dummy FileTask for it,
    // create one
    if (!task) {
      // Create a dummy FileTask only if file actually exists
      if (fs.existsSync(filePath)) {
        stats = fs.statSync(filePath);
        task = new jake.FileTask(filePath);
        task.fullName = fileTaskName;
        task.modTime = stats.mtime;
        task.dummy = true;
        // Put this dummy Task in the global Tasks list so
        // modTime will be eval'd correctly
        jake.Task[fileTaskName] = task;
      }
    }

    return task || null;
  };


  this.init = function () {
    var self = this;
    process.addListener('uncaughtException', function (err) {
      self.program.handleErr(err);
    });

  };

  this.run = function () {
    var args = Array.prototype.slice.call(arguments)
      , program = this.program
      , loader = this.loader
      , preempt
      , opts;

    program.parseArgs(args);
    program.init();

    preempt = program.firstPreemptiveOption();
    if (preempt) {
      preempt();
    }
    else {
      opts = program.opts;
      // Load Jakefile and jakelibdir files
      loader.loadFile(opts.jakefile);
      loader.loadDirectory(opts.jakelibdir);

      program.run();
    }
  };

})());

module.exports = jake;
