var path = require('path');
var util = require('util');
var fs = require('fs');
var childProcess = require('child_process');
var ansi = require("./ansi.js");

var existsSync = fs.existsSync || path.existsSync;

function Builder() {
  this.flagGroups = {};
  this.target = "native_bindings";
  this.sourceFiles = [];
  this.verbose = false;
  this.showWarnings = false;
  this.cppCompiler = "g++";
  this.linker = "g++";
  this.objectFiles = [];

  if(process.platform == 'win32') {
    if(!process.env["VCINSTALLDIR"] && !process.env["VS100COMNTOOLS"]) {
      this.fail("You appear to not be running in a Visual Studio prompt.");
    }
  }

  if(process.platform == 'win32') {
    this.cppCompiler = "cl.exe";
    this.linker = "link.exe";
    this.nodeDir = process.env["NODE_HOME"];
  } else {
    this.nodeDir = process.env["NODE_HOME"] || path.join(process.execPath, '..', '..');
  }

  if(this.nodeDir) {
    this.nodeDir = this.trimQuotes(this.nodeDir);
    this.failIfNotExists(this.nodeDir, 'Node path "%s" not found, try setting NODE_HOME');
  } else {
    this.fail("You must specify NODE_HOME.");
  }

  // process.execPath should equal node.
  if(process.platform == 'win32') {
    this.nodeIncludeDir = path.join(this.nodeDir, 'src');
    this.v8IncludeDir = path.join(this.nodeDir, 'deps/v8/include');
    this.uvIncludeDir = path.join(this.nodeDir, 'deps/uv/include');
    this.nodeLibDir = path.join(this.nodeDir, 'Release');
  } else {

    this.nodeIncludeDir = path.join(this.nodeDir, 'include', 'node');
    this.nodeLibDir = path.join(this.nodeDir, 'lib');
  }
  this.projectDir = path.resolve('.');
  this.buildDir = path.resolve(this.projectDir, 'build');
  this.ouputDir = path.resolve(this.buildDir, 'Release');

  this.appendUnique('CXXFLAGS', '-c');

  if(process.platform == 'win32') {
    this.appendUnique('CXXFLAGS', [
      '-nologo',
      '-DWIN32',
      '-D_WINDOWS',
      '-D_WINDLL',
      '-EHsc',
      '-c',
      '-Oi-',
      '-Od',
      '-Gd',
      '-analyze-'
    ]);
    this.appendUnique('LINKFLAGS', [
      '-nologo',
      '-dll',
      '-MANIFEST:NO',
      '-SUBSYSTEM:WINDOWS',
      '-TLBID:1',
      '-DYNAMICBASE',
      '-NXCOMPAT',
      '-MACHINE:X86',
    ]);
    this.appendLinkerLibrary('node');
    this.appendLinkerLibrary('uv');
  } else {
    this.appendUnique('CXXFLAGS', [
      '-D_LARGEFILE_SOURCE',
      '-D_FILE_OFFSET_BITS=64',
      '-D_GNU_SOURCE',
      '-DPIC',
      '-g',
      '-fPIC',
			'-MD'
    ]);
    if (process.platform == 'darwin') {
      this.appendUnique('LINKFLAGS', [
        '-bundle',
        '-undefined',
        'dynamic_lookup'
      ]);
    } else {
      this.appendUnique('LINKFLAGS', [
        '-shared'
      ]);
    }
  }
}

Builder.prototype.appendLinkerLibrary = function(lib) {
  var flag;
  if(process.platform == 'win32') {
    flag = lib + '.lib';
  } else {
    flag = '-l' + lib;
  }
  this.appendUnique('LINKFLAGS', flag);
}

Builder.prototype.appendLinkerSearchDir = function(dir) {
  var flag;
  if(process.platform == 'win32') {
    flag = '-LIBPATH:' + dir;
  } else {
    flag = '-L' + dir;
  }
  this.appendUnique('LINKFLAGS', flag);
}

Builder.prototype.appendIncludeDir = function(dir) {
  this.appendUnique('CXXFLAGS', '-I' + dir);
}

Builder.prototype.getFlags = function(flagGroupName) {
  var flags = this.flagGroups[flagGroupName];
  if(!flags) {
    flags = this.flagGroups[flagGroupName] = [];
  }
  return flags;
}

Builder.prototype.appendUnique = function(flagGroupName, newFlags) {
  if(!(newFlags instanceof Array)) newFlags = [ newFlags ];
  var flags = this.getFlags(flagGroupName);

  for(var i=0; i<newFlags.length; i++) {
    if(flags.indexOf(newFlags[i]) == -1) {
      flags.push(newFlags[i]);
    }
  }
}

Builder.prototype.appendSource = function(fileName) {
  this.sourceFiles.push(fileName);
}

Builder.prototype.appendSourceDir = function(dirName) {
  var files = fs.readdirSync(dirName);
  for(var i=0; i<files.length; i++) {
    var fileName = files[i];
    if(fileName.match(/\.cpp$/) || fileName.match(/\.c$/) || fileName.match(/\.cxx$/)) {
      this.appendSource(path.join(dirName, fileName));
    }
  }
}

Builder.prototype.getCompilerArgs = function(fileName, outFileName) {
  fileName = path.resolve(fileName);
  this.createDir(path.dirname(outFileName));
  var args = [];
  var flags = this.getFlags('CXXFLAGS');
  args = args.concat(flags);
  args.push(fileName);
  if(process.platform == 'win32') {
    args.push("-Fo" + outFileName);
  } else {
    args.push("-o");
    args.push(outFileName);
  }
  return args;
}

Builder.prototype.getLinkerArgs = function(outFileName) {
  this.createDir(path.dirname(outFileName));
  var args = [];
  var flags = this.getFlags('LINKFLAGS');
  args = args.concat(this.objectFiles);
  if(process.platform == 'win32') {
    args.push("-out:" + outFileName);
  } else {
    args.push("-o");
    args.push(outFileName);
  }
  args = args.concat(flags);
  return args;
}

Builder.prototype.createDir = function(dirName) {
  var parent = path.dirname(dirName);
  if(!existsSync(parent)) {
    this.createDir(parent);
  }
  if(!existsSync(dirName)) {
    fs.mkdirSync(dirName);
  }
}

Builder.prototype._runCommandLine = function(cmd, args, callback) {
  var child = childProcess.spawn(cmd, args);
  child.stdout.on('data', function (data) {
    process.stdout.write(data);
  });
  child.stderr.on('data', function (data) {
    process.stderr.write(data);
  });
  child.on('exit', function(code) {
    callback(code);
  });
}

Builder.prototype._flattenArray = function(arr) {
	var results = [];
	for(var i=0; i<arr.length; i++) {
		results = results.concat(arr[i]);
	}
	return results;
}

Builder.prototype._parseDepFile = function(depData) {
	var deps = depData.split('\n').slice(1).map(function(item) {
		return item
			.replace(/ \\$/, '')
			.replace(/^ /, '')
			.split(' ');
	});
	deps = this._flattenArray(deps);
	deps = deps.filter(function(item) {
		return item.length > 0;
	});
	return deps;
}

Builder.prototype._getMaxTime = function(files) {
	var maxTime = new Date(1900);
	for(var i=0; i<files.length; i++) {
		var stat = fs.statSync(files[i]);
		if(stat.mtime > maxTime) {
			maxTime = stat.mtime;
		}
	}
	return maxTime;
}

Builder.prototype._shouldCompile = function(outFileName, depFileName) {
	try {
		var depFiles = this._parseDepFile(fs.readFileSync(depFileName, 'ascii'));
		var outTime = this._getMaxTime([outFileName]);
		var maxDepTime = this._getMaxTime(depFiles);
		return outTime < maxDepTime;
	} catch(e) {
		//console.log(e);
		return true;
	}
}

Builder.prototype._shouldLink = function(outFileName, objectFiles) {
	try {
		var outTime = this._getMaxTime([outFileName]);
		var maxObjectFileTime = this._getMaxTime(objectFiles);
		return outTime < maxObjectFileTime;
	} catch(e) {
		//console.log(e);
		return true;
	}
}

Builder.prototype._compile = function(curFileIdx, callback) {
  var self = this;
  var fileName = path.resolve(this.sourceFiles[curFileIdx]);
  var outFileName = path.join(this.ouputDir, path.relative(this.projectDir, fileName));
  outFileName = outFileName.replace(/\.cpp$/, '.o');
	var depFileName = outFileName.replace(/\.o$/, '.d');
  this.objectFiles.push(outFileName);

	if(this._shouldCompile(outFileName, depFileName)) {
		console.log(ansi.green(util.format(
			"[%d/%d] cxx: %s -> %s",
			this.currentTask+1,
			this.totalTasks,
			path.relative(this.projectDir, fileName),
			path.relative(this.projectDir, outFileName))));
		var args = this.getCompilerArgs(fileName, outFileName);
		if(this.verbose) {
			console.log(this.cppCompiler, args.join(' '));
		}
		this._runCommandLine(this.cppCompiler, args, function(code) {
			self.currentTask++;
			callback(code);
		});
	} else {
		console.log(ansi.green(util.format(
			"[%d/%d] SKIPPING cxx: %s -> %s",
			this.currentTask+1,
			this.totalTasks,
			path.relative(this.projectDir, fileName),
			path.relative(this.projectDir, outFileName))));
		self.currentTask++;
		callback(0);
	}
}

Builder.prototype.compile = function(callback) {
	this.currentTask = this.currentTask || 0;
  this.totalTasks = this.totalTasks || this.sourceFiles.length;
  callback = callback || function() {};
  var self = this;
  this.createDir(this.ouputDir);

	// append this late because it could be set later in the process
  if(this.showWarnings) {
    this.appendUnique('CXXFLAGS', ['-Wall']);
  }

  // need to append these last to reduce conflicts
  this.appendIncludeDir(this.nodeIncludeDir);

  if(process.platform == 'win32') {
    this.appendIncludeDir(this.v8IncludeDir);
    this.appendIncludeDir(this.uvIncludeDir);
  }

  // no source then fail
  if(this.sourceFiles.length == 0) {
    callback(new Error("Nothing to compile!"));
    return;
  }

  var curFileIdx = 0;
  var doCompile;
  var err = false;
  doCompile = function() {
    if(curFileIdx < self.sourceFiles.length) {
      self._compile(curFileIdx, function(code) {
        if(code != 0) {
          err = true;
        }
        curFileIdx++;
        doCompile();
      });
    } else {
      if(self.verbose) {
        console.log("Done compiling.");
      }
      if(err) {
        callback(new Error("At least one file failed to compile."));
      } else {
        callback();
      }
    }
  }
  doCompile();
}

Builder.prototype.link = function(callback) {
	this.currentTask = this.currentTask || 0;
  this.totalTasks = this.totalTasks || 1;
  callback = callback || function() {};
  var self = this;
  this.createDir(this.ouputDir);

	var outFileName = path.resolve(path.join(this.ouputDir, this.target + ".node"));
	var objectFilesStr = this.objectFiles.map(function(f) { return path.relative(self.projectDir, f); }).join(' ');

  // append last to reduce conflict
  if(process.platform == 'win32') {
    this.appendLinkerSearchDir(path.join(this.nodeLibDir, 'lib'));
  }
  this.appendLinkerSearchDir(this.nodeLibDir);

  // do the linking
	if(this._shouldLink(outFileName, this.objectFiles)) {
		console.log(ansi.yellow(util.format(
			"[%d/%d] cxx_link: %s -> %s",
			this.currentTask+1,
			this.totalTasks,
			objectFilesStr,
			path.relative(this.projectDir, outFileName))));

		var args = this.getLinkerArgs(outFileName);

		if(this.verbose) {
			console.log(this.linker, args.join(' '));
		}

		this._runCommandLine(this.linker, args, function(code) {
			self.currentTask++;
			if(self.verbose) {
				console.log("Done linking.");
			}
			if(code != 0) {
				callback(new Error("Failed to link."));
			} else {
				callback();
			}
		});
	} else {
		console.log(ansi.yellow(util.format(
			"[%d/%d] SKIPPING cxx_link: %s -> %s",
			this.currentTask+1,
			this.totalTasks,
			objectFilesStr,
			path.relative(this.projectDir, outFileName))));
	}
}

Builder.prototype.compileAndLink = function(callback) {
  this.currentTask = 0;
  this.totalTasks = this.sourceFiles.length + 1; // +1 is for linking
  callback = callback || function() {};
  var self = this;
  this.compile(function(err) {
    if(err) { self.fail(err); return; }
    self.link(function(err) {
      if(err) { self.fail(err); return; }
      if(self.verbose) {
        console.log("Done.");
      }
      callback();
    });
  });
}

Builder.prototype.run = function(options, callback) {
	callback = callback || function() {};
	options = options || {};
	this.action = null;

	for(var i=0; i<process.argv.length; i++) {
		var arg = process.argv[i];
		if(arg == '-v' || arg == '--verbose') {
			options.verbose = true;
		} else if(arg == '-Wall' || arg == '--showWarnings') {
			options.showWarnings = true;
		} else if(arg == 'build' || arg == 'compile' || arg == 'link' || arg == 'help') {
			options.action = arg;
		}
	}

	this.verbose = options.verbose;
	this.showWarnings = options.showWarnings;
	this.action = options.action;

	if(this.action == 'build') {
		this.compileAndLink(callback);
	} else if(this.action == 'compile') {
		this.compile(callback);
	} else if(this.action == 'link') {
		this.link(callback);
	} else if(this.action == 'help') {
		this.printHelp();
	} else {
		console.error(ansi.red("No action specified"));
		this.printHelp();
		callback(null);
	}
}

Builder.prototype.printHelp = function() {
	console.log("mnm [options] action");
	console.log("");
	console.log(" Actions");
	console.log("   build    Compile and link the native module");
	console.log("   compile  Only run the compiler step.");
	console.log("   link     Only run the linker step.");
	console.log("");
	console.log(" Options");
	console.log("   -v, --verbose          Print verbose messages.");
	console.log("   -Wall, --showWarnings  Show all compiler warnings.");
	console.log("");
}

Builder.prototype.failIfNotExists = function(dirName, message) {
  dirName = path.resolve(dirName);
  if(!existsSync(dirName)) {
    message = message || "Could not find '%s'.";
    this.fail(message, dirName);
  }
}

Builder.prototype.fail = function(message) {
  if(message instanceof Error) {
    message = message.message;
  }
  var msg = util.format.apply(this, arguments);
  console.error(ansi.red("ERROR: " + msg));
  process.exit(1);
}

Builder.prototype.trimQuotes = function(str) {
  if(str) {
    str = str.replace(/^"/, '').replace(/"$/, '');
  }
  return str;
}

module.exports = Builder;
