var fs = require('fs'),
    inherits = require('util').inherits,
    path = require('path');

var THROW_CONFLICTS = true;

function getPluginPath(config) {
  return (config && config.pluginPath) || path.join(__dirname, '../plugins');
}

function PluginError(message) {
  this.name = 'Plugin Error';
  this.message = this.name + ':\n' + message;
}
inherits(PluginError, Error);

function findEq(collection, option, value) {
  return collection.filter(function(item) {
    return item[option] == value;
  })[0];
}

function mergeByName(acceptor, donor, name, onConflict) {
  if (!donor || !donor.length) return;

  donor.forEach(function(note) {
    var sameOrigNote = findEq(acceptor, name, note[name]);
    if (sameOrigNote) {
      onConflict(sameOrigNote, note);
    } else {
      acceptor.push(note);
    }
  }, this);
}

var cachedPlugins = {};

function _setMockPlugins(pluginPath, plugins) {
  cachedPlugins[pluginPath] = plugins;
}

function getPlugins(config) {
  if (!config || !config.plugins) {
    return [];
  }

  var pluginPath = getPluginPath(config);
  if (cachedPlugins[pluginPath])  {
    return cachedPlugins[pluginPath];
  }

  var dirlist;

  try {
    dirlist = fs.readdirSync(pluginPath);
  } catch (err) {
    dirlist = [];
  }

  var plugins = [];

  dirlist.reduce(function(plugins, subdir) {
    var _path = path.resolve(pluginPath, subdir, 'manifest.json');
    var manifest;

    try {
      manifest = require(_path);

      // This excludes situation where we have two plugins with same name.
      if (subdir !== manifest.name)
        throw new PluginError('Plugin name in manifest.json is different from npm module name');
    } catch (e) {
      console.error('Corrupted manifest.json in %s plugin\n%s\n%s', subdir, e.message, e.stack);
      return plugins;
    }

    validateManifest(manifest);
    plugins.push(manifest);
    return plugins;
  }, plugins);

  cachedPlugins[pluginPath] = plugins;
  return plugins;
}

function validateManifest(manifest) {
  manifest.session = manifest.session || {};
  manifest.protocol = manifest.protocol || {};
  manifest.protocol.domains = manifest.protocol.domains || [];
}

function InspectorJson(config) {
  var inspectorJsonPath = path.join(__dirname, '../front-end/inspector.json'),
      inspectorJson = JSON.parse(fs.readFileSync(inspectorJsonPath)),
      extendedInspectorJsonPath = path.join(__dirname, '../front-end-node/inspector.json'),
      extendedInspectorJson = JSON.parse(fs.readFileSync(extendedInspectorJsonPath));

  this._config = config;
  this._notes = inspectorJson;

  this._merge(extendedInspectorJson);

  if (!config.plugins) return;

  var plugins = getPlugins(config);

  plugins.forEach(function(plugin) {
    var excludes = (plugin.exclude || []).map(function(name) {
      return { name: name, type: 'exclude' };
    });

    var overrides = plugin.override || [];

    var note = {
      name: 'plugins/' + plugin.name,
      type: plugin.type || ''
    };

    var notes = excludes.concat(overrides).concat(note);

    this._merge(notes);
  }, this);
}

InspectorJson.prototype._merge = function(toMergeNotes) {
  var result = [];

  toMergeNotes.forEach(function(note) {
    if (note.type == 'exclude') return;

    result.push(note);
  });

  this._notes.forEach(function(note) {
    var exists = findEq(toMergeNotes, 'name', note.name);
    if (exists) return;

    result.push(note);
  });

  this._notes = result;
};

InspectorJson.prototype.toJSON = function() {
  return this._notes;
};


function ProtocolJson(config) {
  var protocolJsonPath = path.join(__dirname, '../tools/protocol.json'),
      protocolJson = JSON.parse(fs.readFileSync(protocolJsonPath)),
      extendedProtocolJsonPath = path.join(__dirname, '../front-end-node/protocol.json'),
      extendedProtocolJson = JSON.parse(fs.readFileSync(extendedProtocolJsonPath));

  this._config = config;
  this._protocol = protocolJson;
  this._domains = protocolJson.domains;
  this._extendedDomains = extendedProtocolJson.domains;

  if (config.plugins) {
    var plugins = getPlugins(config);
    // At first step we merge all plugins in one protocol.
    // We expect what plugins doesn't have equal methods, events or types,
    // otherwise we throw an error, because this is unsolvable situation.
    plugins.forEach(function(plugin) {
      this._merge(THROW_CONFLICTS, plugin.name, this._extendedDomains, plugin.protocol.domains);
    }, this);
  }

  // At second step we merge plugins with main protocol.
  // Plugins can override original methods, events or types,
  // so we don't need to throw error on conflict, we only print a warning to console.
  this._merge(!THROW_CONFLICTS, '', this._domains, this._extendedDomains);
}

ProtocolJson.prototype._merge = function(throwConflicts, pluginName, origDomains, toMergeDomains) {
  if (!toMergeDomains.length) return;

  var uniqueName = 'domain',
      state = {
        throwConflicts: throwConflicts,
        plugin: pluginName
      };

  mergeByName(
    origDomains,
    toMergeDomains,
    uniqueName,
    this._onDomainConflict.bind(this, state));
};

ProtocolJson.prototype._onDomainConflict = function(state, origDomain, toMergeDomain) {
  state.domain = toMergeDomain.domain;

  ['commands', 'events', 'types'].forEach(function(section) {
    // TODO(3y3): types are unique for protocol, not for domain.
    // We need to register types cache and search in it for conflicts.
    var uniqueName = section == 'types' ? 'id' : 'name',
        origSection = origDomain[section],
        toMergeSection = toMergeDomain[section];

    if (!toMergeSection || !toMergeSection.length)
      return;

    if (!origSection || !origSection.length) {
      origDomain[section] = toMergeSection;
      return;
    }

    state.section = section;
    state.uname = uniqueName;

    mergeByName(
      origSection,
      toMergeSection,
      uniqueName,
      this._onItemConflict.bind(this, state));
  }, this);
};

ProtocolJson.prototype._onItemConflict = function(state, origItem, toMergeItem) {
  if (state.throwConflicts) {
    throw new PluginError(
      'Unresolved conflict in ' + state.section + ' section of `' + state.plugin + '` plugin: ' +
      'item with ' + state.uname + ' `' + toMergeItem[state.uname] + '` already exists.');
  } else {
    console.warn(
      'Item with ' + state.uname + ' `' + toMergeItem[state.uname] + '`' +
      ' in ' + state.section + ' section' +
      ' of ' + state.domain + ' domain' +
      ' was owerriden.');
  }
};

ProtocolJson.prototype.toJSON = function() {
  return this._protocol;
};

function init(config) {
  module.exports.CWD = getPluginPath(config);
}

module.exports = {
  _setMockPlugins: _setMockPlugins,
  getPluginPath: getPluginPath,
  getPlugins: getPlugins,
  validateManifest: validateManifest,
  PluginError: PluginError,
  InspectorJson: InspectorJson,
  ProtocolJson: ProtocolJson,
  init: init
};
