/**
 * This script is for internal `react-boilerplate`'s usage.
 * It will run all generators in order to be able to lint them and detect
 * critical errors. Every generated component's name starts with 'RbGenerated'
 * and any modified file is backed up by a file with the same name but with the
 * 'rbgen' extension so it can be easily excluded from the test coverage reports.
 */

const chalk = require('chalk');
const fs = require('fs');
const nodePlop = require('node-plop');
const path = require('path');
const rimraf = require('rimraf');
const shell = require('shelljs');

const addCheckmark = require('./helpers/checkmark');
const xmark = require('./helpers/xmark');

/**
 * Every generated component/container is preceded by this
 * @type {string}
 */
const { BACKUPFILE_EXTENSION } = require('../generators/index');

process.chdir(path.join(__dirname, '../generators'));

const plop = nodePlop('./index.js');
const componentGen = plop.getGenerator('component');
const containerGen = plop.getGenerator('container');
const languageGen = plop.getGenerator('language');

/**
 * Every generated component/container is preceded by this
 * @type {string}
 */
const NAMESPACE = 'RbGenerated';

/**
 * Return a prettified string
 * @param {*} data
 * @returns {string}
 */
function prettyStringify(data) {
  return JSON.stringify(data, null, 2);
}

/**
 * Handle results from Plop
 * @param {array} changes
 * @param {array} failures
 * @returns {Promise<*>}
 */
function handleResult({ changes, failures }) {
  return new Promise((resolve, reject) => {
    if (Array.isArray(failures) && failures.length > 0) {
      reject(new Error(prettyStringify(failures)));
    }

    resolve(changes);
  });
}

/**
 * Feedback to user
 * @param {string} info
 * @returns {Function}
 */
function feedbackToUser(info) {
  return result => {
    console.info(chalk.blue(info));
    return result;
  };
}

/**
 * Report success
 * @param {string} message
 * @returns {Function}
 */
function reportSuccess(message) {
  return result => {
    addCheckmark(() => console.log(chalk.green(` ${message}`)));
    return result;
  };
}

/**
 * Report errors
 * @param {string} reason
 * @returns {Function}
 */
function reportErrors(reason) {
  // TODO Replace with our own helpers/log that is guaranteed to be blocking?
  xmark(() => console.error(chalk.red(` ${reason}`)));
  process.exit(1);
}

/**
 * Run eslint on all js files in the given directory
 * @param {string} relativePath
 * @returns {Promise<string>}
 */
function runLintingOnDirectory(relativePath) {
  return new Promise((resolve, reject) => {
    shell.exec(
      `npm run lint:eslint "app/${relativePath}/**/**.js"`,
      {
        silent: true,
      },
      code =>
        code
          ? reject(new Error(`Linting error(s) in ${relativePath}`))
          : resolve(relativePath),
    );
  });
}

/**
 * Run eslint on the given file
 * @param {string} filePath
 * @returns {Promise<string>}
 */
function runLintingOnFile(filePath) {
  return new Promise((resolve, reject) => {
    shell.exec(
      `npm run lint:eslint "${filePath}"`,
      {
        silent: true,
      },
      code => {
        if (code) {
          reject(new Error(`Linting errors in ${filePath}`));
        } else {
          resolve(filePath);
        }
      },
    );
  });
}

/**
 * Remove a directory
 * @param {string} relativePath
 * @returns {Promise<any>}
 */
function removeDir(relativePath) {
  return new Promise((resolve, reject) => {
    try {
      rimraf(path.join(__dirname, '/../../src/', relativePath), err => {
        if (err) throw err;
      });
      resolve(relativePath);
    } catch (err) {
      reject(err);
    }
  });
}

/**
 * Remove a given file
 * @param {string} filePath
 * @returns {Promise<any>}
 */
function removeFile(filePath) {
  return new Promise((resolve, reject) => {
    try {
      fs.unlink(filePath, err => {
        if (err) throw err;
      });
      resolve(filePath);
    } catch (err) {
      reject(err);
    }
  });
}

/**
 * Overwrite file from copy
 * @param {string} filePath
 * @param {string} [backupFileExtension=BACKUPFILE_EXTENSION]
 * @returns {Promise<*>}
 */
async function restoreModifiedFile(
  filePath,
  backupFileExtension = BACKUPFILE_EXTENSION,
) {
  return new Promise((resolve, reject) => {
    const targetFile = filePath.replace(`.${backupFileExtension}`, '');
    try {
      fs.copyFile(filePath, targetFile, err => {
        if (err) throw err;
      });
      resolve(targetFile);
    } catch (err) {
      reject(err);
    }
  });
}

/**
 * Test the component generator and rollback when successful
 * @param {string} name - Component name
 * @param {string} type - Plop Action type
 * @returns {Promise<string>} - Relative path to the generated component
 */
async function generateComponent({ name, memo }) {
  const targetFolder = 'components';
  const componentName = `${NAMESPACE}Component${name}`;
  const relativePath = `${targetFolder}/${componentName}`;
  const component = `component/${memo ? 'Pure' : 'NotPure'}`;

  await componentGen
    .runActions({
      name: componentName,
      memo,
      wantMessages: true,
      wantLoadable: true,
    })
    .then(handleResult)
    .then(feedbackToUser(`Generated '${component}'`))
    .catch(reason => reportErrors(reason));
  await runLintingOnDirectory(relativePath)
    .then(reportSuccess(`Linting test passed for '${component}'`))
    .catch(reason => reportErrors(reason));
  await removeDir(relativePath)
    .then(feedbackToUser(`Cleanup '${component}'`))
    .catch(reason => reportErrors(reason));

  return component;
}

/**
 * Test the container generator and rollback when successful
 * @param {string} name - Container name
 * @param {string} type - Plop Action type
 * @returns {Promise<string>} - Relative path to the generated container
 */
async function generateContainer({ name, memo }) {
  const targetFolder = 'containers';
  const componentName = `${NAMESPACE}Container${name}`;
  const relativePath = `${targetFolder}/${componentName}`;
  const container = `container/${memo ? 'Pure' : 'NotPure'}`;

  await containerGen
    .runActions({
      name: componentName,
      memo,
      wantHeaders: true,
      wantActionsAndReducer: true,
      wantSagas: true,
      wantMessages: true,
      wantLoadable: true,
    })
    .then(handleResult)
    .then(feedbackToUser(`Generated '${container}'`))
    .catch(reason => reportErrors(reason));
  await runLintingOnDirectory(relativePath)
    .then(reportSuccess(`Linting test passed for '${container}'`))
    .catch(reason => reportErrors(reason));
  await removeDir(relativePath)
    .then(feedbackToUser(`Cleanup '${container}'`))
    .catch(reason => reportErrors(reason));

  return container;
}

/**
 * Generate components
 * @param {array} components
 * @returns {Promise<[string]>}
 */
async function generateComponents(components) {
  const promises = components.map(async component => {
    let result;

    if (component.kind === 'component') {
      result = await generateComponent(component);
    } else if (component.kind === 'container') {
      result = await generateContainer(component);
    }

    return result;
  });

  const results = await Promise.all(promises);

  return results;
}

/**
 * Test the language generator and rollback when successful
 * @param {string} language
 * @returns {Promise<*>}
 */
async function generateLanguage(language) {
  // Run generator
  const generatedFiles = await languageGen
    .runActions({ language, test: true })
    .then(handleResult)
    .then(feedbackToUser(`Added new language: '${language}'`))
    .then(changes =>
      changes.reduce((acc, change) => {
        const pathWithRemovedAnsiEscapeCodes = change.path.replace(
          /* eslint-disable-next-line no-control-regex */
          /(\u001b\[3(?:4|9)m)/g,
          '',
        );
        const obj = {};
        obj[pathWithRemovedAnsiEscapeCodes] = change.type;
        return Object.assign(acc, obj);
      }, {}),
    )
    .catch(reason => reportErrors(reason));

  // Run eslint on modified and added JS files
  const lintingTasks = Object.keys(generatedFiles)
    .filter(
      filePath =>
        generatedFiles[filePath] === 'modify' ||
        generatedFiles[filePath] === 'add',
    )
    .filter(filePath => filePath.endsWith('.js'))
    .map(async filePath => {
      const result = await runLintingOnFile(filePath)
        .then(reportSuccess(`Linting test passed for '${filePath}'`))
        .catch(reason => reportErrors(reason));

      return result;
    });

  await Promise.all(lintingTasks);

  // Restore modified files
  const restoreTasks = Object.keys(generatedFiles)
    .filter(filePath => generatedFiles[filePath] === 'backup')
    .map(async filePath => {
      const result = await restoreModifiedFile(filePath)
        .then(
          feedbackToUser(
            `Restored file: '${filePath.replace(
              `.${BACKUPFILE_EXTENSION}`,
              '',
            )}'`,
          ),
        )
        .catch(reason => reportErrors(reason));

      return result;
    });

  await Promise.all(restoreTasks);

  // Remove backup files and added files
  const removalTasks = Object.keys(generatedFiles)
    .filter(
      filePath =>
        generatedFiles[filePath] === 'backup' ||
        generatedFiles[filePath] === 'add',
    )
    .map(async filePath => {
      const result = await removeFile(filePath)
        .then(feedbackToUser(`Removed '${filePath}'`))
        .catch(reason => reportErrors(reason));

      return result;
    });

  await Promise.all(removalTasks);

  return language;
}

/**
 * Run
 */
(async function () {
  await generateComponents([
    { kind: 'component', name: 'Component', memo: false },
    { kind: 'component', name: 'MemoizedComponent', memo: true },
    { kind: 'container', name: 'Container', memo: false },
    { kind: 'container', name: 'MemoizedContainer', memo: true },
  ]).catch(reason => reportErrors(reason));

  await generateLanguage('fr').catch(reason => reportErrors(reason));
})();
