var Promise = require('promise');
var fs = require('fs');
var path = require('path');
var normalize = path.normalize;


Promise.prototype.nodeify = function (cb) {
  if (typeof cb === 'function') {
    this.then(function (res) { process.nextTick(function () { cb(null, res); }); },
              function (err) { process.nextTick(function () { cb(err); }); });
    return undefined;
  } else {
    return this;
  }
}

var minifiers = {};

module.exports = Transformer;
function Transformer(obj) {
  this.name = obj.name;
  this.engines = obj.engines;
  this.isBinary = obj.isBinary || false;
  this.isMinifier = obj.isMinifier || false;
  this.outputFormat = obj.outputFormat;
  this._cache = {};
  if (typeof obj.async === 'function') {
    this._renderAsync = obj.async;
    this.sudoSync = obj.sudoSync || false;
  }
  if (typeof obj.sync === 'function') {
    this._renderSync = obj.sync;
    this.sync = true;
  } else {
    this.sync = obj.sudoSync || false;
  }

  if (this.isMinifier)
    minifiers[this.outputFormat] = this;
  else {
    var minifier = minifiers[this.outputFormat];
    if (minifier) {
      this.minify = function(str, options) {
        if (options && options.minify)
          return minifier.renderSync(str, typeof options.minify === 'object' && options.minify || {});
        return str;
      }
    }
  }
}

Transformer.prototype.cache = function (options, data) {
  if (options.cache && options.filename) {
    if (data) return this.cache[options.filename] = data;
    else return this.cache[options.filename];
  } else {
    return data;
  }
};
Transformer.prototype.loadModule = function () {
  if (this.engine) return this.engine;
  for (var i = 0; i < this.engines.length; i++) {
    try {
      var res = this.engines[i] === '.' ? null : (this.engine = require(this.engines[i]));
      this.engineName = this.engines[i];
      return res;
    } catch (ex) {
      if (this.engines.length === 1) {
        throw ex;
      }
    }
  }
  throw new Error('In order to apply the transform ' + this.name + ' you must install one of ' + this.engines.map(function (e) { return '"' + e + '"'; }).join());
};
Transformer.prototype.minify = function(str, options) {
  return str;
}
Transformer.prototype.renderSync = function (str, options) {
  options = options || {};
  options = clone(options);
  this.loadModule();
  if (this._renderSync) {
    return this.minify(this._renderSync((this.isBinary ? str : fixString(str)), options), options);
  } else if (this.sudoSync) {
    options.sudoSync = true;
    var res, err;
    this._renderAsync((this.isBinary ? str : fixString(str)), options, function (e, val) {
      if (e) err = e;
      else res = val;
    });
    if (err) throw err;
    else if (res != undefined) return this.minify(res, options);
    else if (typeof this.sudoSync === 'string') throw new Error(this.sudoSync.replace(/FILENAME/g, options.filename || ''));
    else throw new Error('There was a problem transforming ' + (options.filename || '') + ' syncronously using ' + this.name);
  } else {
    throw new Error(this.name + ' does not support transforming syncronously.');
  }
};
Transformer.prototype.render = function (str, options, cb) {
  options = options || {};
  var self = this;
  return new Promise(function (resolve, reject) {
    self.loadModule();
    if (self._renderAsync) {
      self._renderAsync((self.isBinary ? str : fixString(str)), clone(options), function (err, val) {
        if (err) reject(err);
        else resolve(self.minify(val, options));
      })
    } else {
      resolve(self.renderSync(str, options));
    }
  })
  .nodeify(cb);
};
Transformer.prototype.renderFile = function (path, options, cb) {
  options = options || {};
  var self = this;
  return new Promise(function (resolve, reject) {
    options.filename = (path = normalize(path));
    if (self._cache[path])
      resolve(null);
    else
      fs.readFile(path, function (err, data) {
        if (err) reject(err);
        else resolve(data);
      })
  })
  .then(function (str) {
    return self.render(str, options);
  })
  .nodeify(cb);
};
Transformer.prototype.renderFileSync = function (path, options) {
  options = options || {};
  options.filename = (path = normalize(path));
  return this.renderSync((this._cache[path] ? null : fs.readFileSync(path)), options);
};
function fixString(str) {
  if (str == null) return str;
  //convert buffer to string
  str = str.toString();
  // Strip UTF-8 BOM if it exists
  str = (0xFEFF == str.charCodeAt(0) 
    ? str.substring(1)
    : str);
  //remove `\r` added by windows
  return str.replace(/\r/g, '');
}

function clone(obj) {
  if (Array.isArray(obj)) {
    return obj.map(clone);
  } else if (obj && typeof obj === 'object') {
    var res = {};
    for (var key in obj) {
      res[key] = clone(obj[key]);
    }
    return res;
  } else {
    return obj;
  }
}
