/* global module */
/* jslint node: true */
/* jslint indent: 2 */
'use strict';

/* Module Require */
var fs = require('fs'),
  path = require('path'),
  teeft = require('rd-teeft'),
  request = require('request'),
  mkdirp = require('mkdirp'),
  async = require('async');

/* Libs */
var jLouvain = require('./lib/jLouvain.js');

/**
 * Objet regroupant les fonctionnalités liées aux tdm
 */
var utils = {};

/**
 * Classe Représentant un corpus
 */
class Corpus {
  /*
   * Constructeur
   * @param {Object} options Objet contenant les différents paramètres possible de l'objet, à savoir :
   *   - {String} name : Nom du corpus
   *   - {String} file : Nom du fichier contenant les Ids ISTEX
   *   - {String} in : Répertoire d'entrée (contenant les fichiers téléchargés à partir du fichier .txt)
   *   - {String} out : Répertoire de sortie prévu
   * @return {Corpus} Return un objet de type Corpus
   */
  constructor(options) {
    if (!options) return;
    this.name = (typeof options.name === 'string') ? options.name :  "";
    this.file = (typeof options.file === 'string') ? options.file :  "";
    this.in = (typeof options.in === 'string') ? options.in : "";
    this.out = (typeof options.out === 'string') ? options.out : "";
  }
}

/**
 * Objet permettant de traiter un ensemble de Corpus
 */
utils.corpusManager = {};

/**
 * Prépare les données des corpus (init + harvest des fichiers)
 * @param {String} inputDir Chemin vers le répertoire contenant des corpus (un ou plusieurs fichiers .txt contenant des IDs ISTEX)
 * @param {String} outputDir Chemin vers le répertoire qui contiendra l'architecture créée
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreurs lors du traitement
 *  - {Array} res Liste des Corpus correctement traités
 * @return {undefined} Return undefined
 */
utils.corpusManager.prepare = function(inputDir, outputDir, cb) {
  return utils.corpusManager.init(inputDir, outputDir, function(err, corpora) { // Init des corpus
    if (err) return cb(err);
    var result = []; // Tableau qui contiendra tous les fichiers traités
    return async.each(corpora, function(corpus, next) {
      return utils.corpusManager.harvest(corpus.file, corpus.in, function(err, res) { // Harvest du corpus
        if (err) return next(err);
        result.push(corpus);
        return next();
      });
    }, function(err) {
      if (err) return cb(err, result);
      return cb(null, result);
    });
  });
};

/**
 * Initialise les chemins d'un outil de TDM dans EzMaster
 * @param {String} inputDir Chemin vers le répertoire contenant des corpus (un ou plusieurs fichiers .txt contenant des IDs ISTEX)
 * @param {String} outputDir Chemin vers le répertoire qui contiendra l'architecture créée
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreur de Lecture/Écriture
 *  - {Array} res Liste des fichiers correctements traités
 * @return {undefined} Return undefined
 * Exemple d'architecture crée pour 2 corpus x et y : 
 * ./
 *  x.txt
 *  y.txt
 *  in/
 *     x/
 *     y/
 *  out/
 *     x/
 *     y/
 */
utils.corpusManager.init = function(inputDir, outputDir, cb) {
  // Parcour du répertoire devant contenir les fichier txt
  return fs.readdir(inputDir, function(err, files) {
    if (err) return cb(err); // Répertoire contenant les fichiers 'corpus' invalide, renvois d'une erreur
    var result = []; // Tableau qui contiendra tous les fichiers traités
    return async.each(files, function(file, callback) {
      if (!file.match(/[\w\W]+.txt$/)) return callback(err); // Nom invalide, on l'ignore
      var corpusName = path.basename(file, '.txt'), // [PROVISOIRE] Nom du répertoire = Nom du fichier (sans l'extension)
        fullPath = path.join(inputDir, file); // Chemin complet du fichier à partir de l'inputDir
      return fs.stat(fullPath, function(err, stats) {
        if (err) return callback(err); // Erreur lors du stats, on la remonte
        if (!stats.isFile()) return callback(); // Ce n'est pas un fichier, on l'ignore
        return fs.readFile(fullPath, 'utf-8', function(err, res) {
          if (err) return callback(err); // Erreur lors du readFile, on la remonte
          var outputFile = path.join(outputDir, file); // Erreur lors du readFile, on la remonte
          return fs.writeFile(outputFile, res, 'utf-8', function(err) {
            if (err) return callback(err); // Erreur lors du writeFile, on la remonte
            var corpus = new Corpus({
              'name': corpusName,
              'file': outputFile
            });
            return async.each(['in', 'out'], function(subDirectory, next) { // Création d'un répertoire dans in et out
              var dir = path.join(outputDir, subDirectory, corpusName); // Chemin complet du répertoire
              mkdirp(dir,
                function(err) {
                  if (err) return next(err); // Erreur lors du mkdirp, on la remonte
                  corpus[subDirectory] = dir;
                  return next();
                });
            }, function(err) {
              if (err) return callback(err); // Renvois de toutes les erreurs remontée lors de la création des répertoires
              result.push(corpus); // Corpus correctement traité
              return callback();
            });
          });
        });
      });
    }, function(err) {
      if (err) return cb(err, result); // Renvois de toutes les erreurs remontée lors de la création des répertoires + copie du fichier
      return cb(null, result); // Tout c'est bien passé!
    });
  });
};

/**
 * Harvest les fichiers d'un corpus
 * @param {String} inputFile Fichier d'entré contenant des Id ISTEX
 * @param {String} outputDir Répertoire de sortie où écrire les pdf
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreurs lors du traitement
 *  - {Array} res Liste des Ids correctement traités
 * @return {undefined} Return undefined
 */
utils.corpusManager.harvest = function(inputFile, outputDir, cb) {
  return fs.readFile(inputFile, 'utf-8', function(err, res) { // Lecture du fichier d'entrée
    var ids = res.split('\n'), // Split des Id (un Id / Ligne)
      result = []; // Tableau qui contiendra tous les Id traités
    return async.eachSeries(ids, function(id, next) { // Pour chaque Id
      if (!id.match(/[0-9A-Fa-f]{40}/)) return next('Invalid ISTEX Id : ' + id); // Check si l'Id est valide
      var url = 'https://api.istex.fr/document/' + id + '/fulltext/txt'; // Construction de l'url
      return request(url, function(error, response, body) {
        if (error) return next(error); // Si erreurd de request, on la remonte
        if (response.statusCode.toString()[0] !== '2') return next(response); // Si erreur http (code différent de 2XX, on la remonte
        var fullPath = path.join(outputDir, id + '.txt'); // Calcul du chemin complet du fichier téléchargé
        return fs.writeFile(fullPath, body, function(err, res) {
          if (err) return next(err); // Si erreur lors de l'écriture, on remonte l'erreur
          result.push(id); // Sinon on considère l'Id comme 'done'
          return next();
        });
      });
    }, function(err) {
      if (err) return cb(err, result); // Renvoi de toutes les erreurs remontée lors de la création des répertoires + copie du fichier
      return cb(null, result);
    });
  });
};

/**
 * Indexe tous les fichiers d'un corpus
 * @param {String} inputDir Répertoire d'entré contenant des fichiers .txt
 * @param {String} outputFile Fichier de sortie
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreurs lors du traitement
 *  - {Array} res Liste des Indexations
 * @return {undefined} Return undefined
 */
utils.corpusManager.indexAll = function(inputDir, outputFile, cb) {
  var result = []; // List of indexations
  fs.readdir(inputDir, function(err, filenames) {
    if (err) return cb(err); // I/O Errors
    async.each(filenames, function(filename, callback) {
        var filePath = path.join(inputDir, filename);
        fs.readFile(filePath, 'utf-8', function(err, res) {
          if (err) return callback(err); // I/O Errors
          var docId = path.basename(filename, ('.txt'));
          result.push({
            'id': docId,
            'keywords': teeft.index(res).keywords
          });
          callback();
        });
      },
      function(err) {
        if (err) return cb(err); // I/O Errors
        // write data
        fs.writeFile(outputFile, JSON.stringify(result), 'utf-8', function(err, res) {
          if (err) return cb(err);
          return cb(null, result);
        });
      });
  });
};

utils.graphs = {};

/**
 * Génère un graph de Documents (Visualisation des lien entre chaque documents)
 * @param {Array} indexations Liste d'indexation
 * @param {Object} options Données optionnelles
 * @param {function} cb Callback appelée à la fin du traitement, avec comme paramètre disponible :
 *  - {Error} err Erreurs lors du traitement
 *  - {Object} res Données affichables avec d3.js
 * @return {undefined} Return undefined
 */
utils.graphs.docToDoc = function(indexations, options, cb) {
  if (!options) options = {};
  var terms = {}, // Each key is a term, his value is the list of documents containing it
    docIds = [], // List of document Ids
    result = {
      'nodes': [],
      'links': []
    },
    edges = [], //  [{'source': '', 'target': '', 'weight': 0}, ...]
    nodes = [], // ['id', ...]
    matrix = {}, // Matrix of "doc-doc" links (sparse matrix)
    output = options.output || './cache/docToDoc.json',
    minLinkValue = options.minLinkValue || 0;
  // Construction of terms Object
  for (var i = 0; i < indexations.length; i++) {
    var id = indexations[i].id,
      keywords = indexations[i].keywords;
    for (var j = 0; j < keywords.length; j++) {
      var term = keywords[j].term;
      if (!terms[term]) terms[term] = [];
      terms[term].push(i);
    }
  }
  // Construction of matrix Object
  for (var key in terms) {
    // Fill it with values
    for (var i = 0; i < terms[key].length - 1; i++) {
      var idDoc1 = terms[key][i];
      for (var j = i + 1; j < terms[key].length; j++) {
        var idDoc2 = terms[key][j],
          ids = [idDoc1, idDoc2],
          id = {
            'min': Math.min(ids[0], ids[1]),
            'max': Math.max(ids[0], ids[1])
          };
        // Only half of it will be fill!
        if (!matrix[id.min + ',' + id.max]) {
          matrix[id.min + ',' + id.max] = 0;
        }
        matrix[id.min + ',' + id.max]++;
      }
    }
  }
  // Construction of matrix of links doc-doc
  for (var key in matrix) {
    var ids = key.split(',');
    if (matrix[key] >= minLinkValue) {
      edges.push({
        'source': ids[0],
        'target': ids[1],
        'weight': matrix[key]
      });
      result.links.push({
        'source': ids[0],
        'target': ids[1],
        'value': matrix[key]
      });
    }
  }
  // Construction of Nodes object
  for (var i = 0; i < indexations.length; i++) {
    var id = indexations[i].id,
      keywords = indexations[i].keywords;
    var max = (keywords.length < 5) ? keywords.length : 5;
    nodes.push(i);
    result.nodes.push({
      'id': i,
      'istex': id,
      'value': keywords.slice(0, max).map(function(elem, i) {
        return elem.term
      }).join('; '),
      'group': 0
    });
  }
  // Create the "community"
  var community = jLouvain().nodes(nodes).edges(edges),
    res = community();
  // Affect community for each node
  for (var key in res) {
    result.nodes[key].group = res[key];
  }
  // write data
  fs.writeFile(output, JSON.stringify(result), 'utf-8', function(err, res) {
    if (err) return cb(err);
    return cb(null, result);
  });
};

module.exports = utils;