'use strict';

const {WError} = require('error');
const fs = require('fs');
const path = require('path');
const findUp = require('find-up');
const minimatch = require('minimatch');
const OOOTransform = require('./out-of-order-transform');

class YoResolveError extends WError {}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Creates a out of order transform.
 *
 * @param {Function} transform - transform function.
 * @param {Object}   options   - additional options passed to transform
 * @return {Stream} a transform stream.
 */
function createFileTransform(
  transform = (file, _enc, cb) => cb(null, file),
  options = {}
) {
  return new OOOTransform({
    ...options,
    transform(...args) {
      return transform.call(this, ...args);
    }
  });
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Detect if the file is modified
 * See https://github.com/SBoudrias/mem-fs-editor/blob/3ff18e26c52dc30d8f371bcc72c1884f2ea706d6/lib/actions/commit.js#L38
 *
 * @param {Object} field - vinyl file
 */
function fileIsModified(file) {
  // New files (don't exists in the disc) that have been deleted won't be committed and is considered unmodified file.
  return (file.state === 'modified' || (file.state === 'deleted' && !file.isNew));
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Create a for each file stream transform.
 * @param {Function} forEach - Function to execute for each file
 * @param {Object} options - Options
 * @param {boolean} [options.autoForward = true] - Set false to don't add a file to the stream, the function must do it.
 * @param {boolean} [options.executeUnmodified = false] - Set true to execute the forEach function with a not modified file.
 * @param {boolean} [options.forwardUmodified = true] - Set false to don't add a not modified file to the stream.
 * @return {Transform} A Transform https://nodejs.org/api/stream.html#stream_class_stream_transform
 */
function createEachFileTransform(forEach, options = {}) {
  if (typeof forEach !== 'function') {
    options = forEach;
    forEach = () => {};
  }
  const {forwardUmodified = true, executeUnmodified = false, autoForward = true} = options;
  return createFileTransform(function (file, enc, cb) {
    const forward = () => {
      if (autoForward && (forwardUmodified || fileIsModified(file))) {
        this.push(file);
      }
    };
    if (!executeUnmodified && !fileIsModified(file)) {
      forward();
      return;
    }
    return Promise
      .resolve(forEach.call(this, file, enc))
      .then(() => forward())
      .catch(error => cb(error));
  }, options);
}

function parseYoAttributesFile(yoAttributeFileName) {
  let overridesContent;
  try {
    overridesContent = fs.readFileSync(yoAttributeFileName, 'utf-8');
  } catch (error) {
    throw YoResolveError.wrap('Error loading yo attributes file {yoAttributeFileName}', error, {yoAttributeFileName});
  }
  const absoluteDir = path.dirname(yoAttributeFileName);
  return Object.fromEntries(
    overridesContent
      .split(/\r?\n/)
      .map(override => override.trim())
      .map(override => override.split('#')[0].trim())
      .filter(override => override)
      .map(override => override.split(/[\s+=]/))
      .map(([pattern, status = 'skip']) => [path.join(absoluteDir, pattern), status])
  );
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 */
function createConflicterCheckTransform(conflicter) {
  return createEachFileTransform(file => conflicter.checkForCollision(file), {logName: 'environment:conflicter-check'});
}

/**
 * @private
 */
function getConflicterStatusForFile(conflicter, filePath, yoAttributeFileName = '.yo-resolve') {
  const fileDir = path.dirname(filePath);
  conflicter.yoResolveByFile = conflicter.yoResolveByFile || {};
  const yoResolveFiles = [];
  let foundYoAttributesFile = findUp.sync([yoAttributeFileName], {cwd: fileDir});
  while (foundYoAttributesFile) {
    yoResolveFiles.push(foundYoAttributesFile);
    foundYoAttributesFile = findUp.sync([yoAttributeFileName], {cwd: path.join(path.dirname(foundYoAttributesFile), '..')});
  }

  let fileStatus;
  if (yoResolveFiles) {
    yoResolveFiles.forEach(yoResolveFile => {
      if (conflicter.yoResolveByFile[yoResolveFile] === undefined) {
        conflicter.yoResolveByFile[yoResolveFile] = parseYoAttributesFile(yoResolveFile);
      }
    });
    yoResolveFiles
      .map(yoResolveFile => conflicter.yoResolveByFile[yoResolveFile])
      .map(attributes => attributes)
      .find(yoResolve => {
        return Object.entries(yoResolve).some(([pattern, status]) => {
          if (minimatch(filePath, pattern)) {
            fileStatus = status;
            return true;
          }
          return false;
        });
      });
  }
  return fileStatus;
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Create a yo-resolve transform stream.
 * Suports pre-defined conflicter actions action based on file glob.
 * @param {Conflicter} conflicter - Conflicter instance
 * @param {string} yoResolveFileName - .yo-resolve filename
 * @return {Transform} A Transform https://nodejs.org/api/stream.html#stream_class_stream_transform
 */
function createYoResolveTransform(conflicter, yoResolveFileName) {
  return createEachFileTransform(file => {
    file.conflicter = file.conflicter || getConflicterStatusForFile(conflicter, file.path, yoResolveFileName);
  }, {logName: 'environment:yo-resolve'});
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Create a force yeoman configs transform stream.
 * @return {Transform} A Transform https://nodejs.org/api/stream.html#stream_class_stream_transform
 */
function createYoRcTransform() {
  return createEachFileTransform(file => {
    const filename = path.basename(file.path);
    // Config file should not be processed by the conflicter. Force override.
    if (filename === '.yo-rc.json' || filename === '.yo-rc-global.json') {
      file.conflicter = 'force';
    }
  }, {logName: 'environment:yo-rc'});
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 *
 * Create a transform to apply conflicter status.
 * @param {Log} logger - Log reference. See log.js
 * @return {Transform} A Transform https://nodejs.org/api/stream.html#stream_class_stream_transform
 */
function createConflicterStatusTransform() {
  return createFileTransform(function (file) {
    const action = file.conflicter;

    delete file.conflicter;
    delete file.binary;
    delete file.conflicterChanges;
    delete file.conflicterLog;

    if (!action && file.state) {
      this.push(file);
      return;
    }

    if (action === 'skip') {
      delete file.state;
      delete file.isNew;
    } else {
      this.push(file);
    }
  }, {logName: 'environment:conflicter-status'});
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 */
function createModifiedTransform() {
  return createEachFileTransform({forwardUmodified: false, logName: 'environment:modified'});
}

/**
 * Transform api should be avoided by generators without a executable.
 * May break between major yo versions.
 */
function createCommitTransform(memFsEditor) {
  return createFileTransform(file => memFsEditor.commitFileAsync(file), {logName: 'environment:commit'});
}

module.exports = {
  createFileTransform,
  fileIsModified,
  getConflicterStatusForFile,
  createEachFileTransform,
  createYoResolveTransform,
  createYoRcTransform,
  createModifiedTransform,
  createCommitTransform,
  createConflicterCheckTransform,
  createConflicterStatusTransform
};