/**
 * Copyright 2013-2021 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see https://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* eslint-disable no-console */

const path = require('path');
const shelljs = require('shelljs');
const ejs = require('ejs');
const _ = require('lodash');
const fs = require('fs');
const crypto = require('crypto');
const os = require('os');

const constants = require('./generator-constants');
const FileUtils = require('../jdl/utils/file-utils');

const LANGUAGES_MAIN_SRC_DIR = `${__dirname}/languages/templates/${constants.CLIENT_MAIN_SRC_DIR}`;

module.exports = {
  rewrite,
  rewriteFile,
  replaceContent,
  classify,
  rewriteJSONFile,
  copyWebResource,
  renderContent,
  deepFind,
  getJavadoc,
  buildEnumInfo,
  getEnumInfo,
  copyObjectProps,
  decodeBase64,
  getDBTypeFromDBValue,
  getBase64Secret,
  getRandomHex,
  checkStringInFile,
  checkRegexInFile,
  loadYoRc,
  packageNameToNamespace,
  stringHashCode,
  gitExec,
  isGitInstalled,
  vueReplaceTranslation,
  vueAddPageToRouterImport,
  vueAddPageToRouter,
  vueAddPageServiceToMainImport,
  vueAddPageServiceToMain,
  vueAddPageProtractorConf,
  languageSnakeCase,
  languageToJavaLanguage,
  addSectionsCondition,
  mergeSections,
};

const databaseTypes = require('../jdl/jhipster/database-types');

const SQL = databaseTypes.SQL;

/**
 * Rewrite file with passed arguments
 * @param {object} args argument object (containing path, file, haystack, etc properties)
 * @param {object} generator reference to the generator
 */
function rewriteFile(args, generator) {
  let fullPath;
  if (args.path) {
    fullPath = generator.destinationPath(path.join(args.path, args.file));
  } else {
    fullPath = generator.destinationPath(args.file);
  }
  if (!generator.env.sharedFs.existsInMemory(fullPath) && generator.env.sharedFs.existsInMemory(`${fullPath}.jhi`)) {
    fullPath = `${fullPath}.jhi`;
  }

  args.haystack = generator.fs.read(fullPath);
  const body = rewrite(args);
  generator.fs.write(fullPath, body);
  return args.haystack !== body;
}

/**
 * Replace content
 * @param {object} args argument object
 * @param {object} generator reference to the generator
 */
function replaceContent(args, generator) {
  let fullPath = generator.destinationPath(args.file);
  if (!generator.env.sharedFs.existsInMemory(fullPath) && generator.env.sharedFs.existsInMemory(`${fullPath}.jhi`)) {
    fullPath = `${fullPath}.jhi`;
  }

  const re = args.regex ? new RegExp(args.pattern, 'g') : args.pattern;

  const currentBody = generator.fs.read(fullPath);
  const newBody = currentBody.replace(re, args.content);
  generator.fs.write(fullPath, newBody);
  return newBody !== currentBody;
}

/**
 * Escape regular expressions.
 *
 * @param {string} str string
 * @returns {string} string with regular expressions escaped
 */
function escapeRegExp(str) {
  return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); // eslint-disable-line
}

/**
 * Normalize line endings.
 * If in Windows is Git autocrlf used then need to replace \r\n with \n
 * to achieve consistent comparison result when comparing strings read from file.
 *
 * @param {string} str string
 * @returns {string} string where CRLF is replaced with LF in Windows
 */
function normalizeLineEndings(str) {
  const isWin32 = os.platform() === 'win32';
  return isWin32 ? str.replace(/\r\n/g, '\n') : str;
}

/**
 * Change spaces sequences and '>' to allow any number of spaces or new line prefix
 *
 * @param {string} str string
 * @returns {string} string where CRLF is replaced with LF in Windows
 */
function convertToPrettierExpressions(str) {
  return str.replace(/\s+/g, '([\\s\n]*)').replace(/>+/g, '(\n?[\\s]*)>');
}

/**
 * Rewrite using the passed argument object.
 *
 * @param {object} args arguments object (containing splicable, haystack, needle properties) to be used
 * @param {string[]} args.splicable       - content to be added.
 * @param {boolean} [args.prettierAware]  - apply prettier aware expressions before looking for applied needles.
 * @param {string|RegExp} [args.regexp]    - use another content for looking for applied needles.
 * @returns {*} re-written file
 */
function rewrite(args) {
  // check if splicable is already in the body text
  let re;
  if (args.regexp) {
    re = args.regexp;
    if (!re.test) {
      re = escapeRegExp(re);
    }
  } else {
    re = args.splicable.map(line => `\\s*${escapeRegExp(normalizeLineEndings(line))}`).join('\n');
  }
  if (!re.test) {
    if (args.prettierAware) {
      re = convertToPrettierExpressions(re);
    }
    re = new RegExp(re);
  }

  if (re.test(normalizeLineEndings(args.haystack))) {
    return args.haystack;
  }

  const lines = args.haystack.split('\n');

  let otherwiseLineIndex = -1;
  lines.forEach((line, i) => {
    if (line.includes(args.needle)) {
      otherwiseLineIndex = i;
    }
  });

  if (otherwiseLineIndex === -1) {
    console.warn(`Needle ${args.needle} not found at file ${args.file}`);
    return args.haystack;
  }

  let spaces = 0;
  while (lines[otherwiseLineIndex].charAt(spaces) === ' ') {
    spaces += 1;
  }

  let spaceStr = '';

  // eslint-disable-next-line no-cond-assign
  while ((spaces -= 1) >= 0) {
    spaceStr += ' ';
  }

  lines.splice(otherwiseLineIndex, 0, args.splicable.map(line => spaceStr + line).join('\n'));

  return lines.join('\n');
}

/**
 * Convenient function to convert string into valid java class name
 * Note: _.classify uses _.titleize which lowercase the string,
 * so if the user chooses a proper ClassName it will not rename properly
 *
 * @param string string to 'class'-ify
 * @returns {string} 'class'-ified string
 */
function classify(string) {
  string = string.replace(/[\W_](\w)/g, match => ` ${match[1].toUpperCase()}`).replace(/\s/g, '');
  return string.charAt(0).toUpperCase() + string.slice(1);
}

/**
 * Rewrite JSON file
 *
 * @param {string} filePath file path
 * @param {function} rewriteFile rewriteFile function
 * @param {object} generator reference to the generator
 */
function rewriteJSONFile(filePath, rewriteFile, generator) {
  filePath = generator.destinationPath(filePath);
  const jsonObj = generator.fs.readJSON(filePath);
  rewriteFile(jsonObj, generator);
  generator.fs.writeJSON(filePath, jsonObj, null, 2);
}

/**
 * Copy web resources
 *
 * @param {string} source source
 * @param {string} dest destination
 * @param {regex} regex regex
 * @param {string} type type of resource (html, js, etc)
 * @param {object} generator reference to the generator
 * @param {object} opt options
 * @param {any} template template
 */
function copyWebResource(source, dest, regex, type, generator, opt = {}, template) {
  if (generator.enableTranslation) {
    generator.template(source, dest, generator, opt);
  } else {
    dest = generator.destinationPath(dest);
    if (!dest) {
      return;
    }
    renderContent(source, generator, generator, opt, body => {
      body = body.replace(regex, '');
      switch (type) {
        case 'html':
          body = replacePlaceholders(body, generator);
          break;
        case 'js':
          body = replaceTitle(body, generator);
          if (dest.endsWith('error.route.ts')) {
            body = replaceErrorMessage(body, generator);
          }
          break;
        case 'jsx':
          body = replaceTranslation(body, generator);
          break;
        default:
          break;
      }
      generator.fs.write(dest, body);
    });
  }
}

/**
 * Render content
 *
 * @param {string} source source
 * @param {object} generator reference to the generator
 * @param {any} context context
 * @param {object} options options
 * @param {function} [cb] callback function
 * @return {Promise<String>} Promise rendered content
 */
function renderContent(source, generator, context, options, cb) {
  options = {
    root: options.root || generator.jhipsterTemplatesFolders || generator.templatePath(),
    context: generator,
    ...options,
  };
  if (context.entityClass) {
    const basename = path.basename(source);
    if (context.configOptions && context.configOptions.sharedEntities) {
      Object.values(context.configOptions.sharedEntities).forEach(entity => {
        entity.resetFakerSeed(`${context.entityClass}-${basename}`);
      });
    } else if (context.resetFakerSeed) {
      context.resetFakerSeed(basename);
    }
  }
  const promise = ejs.renderFile(generator.templatePath(source), context, options);
  if (cb) {
    return promise
      .then(res => cb(res))
      .catch(err => {
        generator.warning(`Copying template ${source} failed. [${err}]`);
        throw err;
      });
  }
  return promise;
}

/**
 *
 * @param {string} body html body
 * @param {object} generator reference to the generator
 * @returns string with pageTitle replaced
 */
function replaceTitle(body, generator) {
  const regex = /pageTitle[\s]*:[\s]*['|"]([a-zA-Z0-9.\-_]+)['|"]/g;
  return replaceTranslationKeysWithText(body, generator, regex);
}

/**
 *
 * @param {string} body html body
 * @param {object} generator reference to the generator
 * @returns string with pageTitle replaced
 */
function replaceErrorMessage(body, generator) {
  const regex = /errorMessage[\s]*:[\s]*['|"]([a-zA-Z0-9.\-_]+)['|"]/g;
  return replaceTranslationKeysWithText(body, generator, regex);
}

/**
 *
 * @param {string} body html body
 * @param {object} generator reference to the generator
 * @param {object} regex regular expression to find keys
 * @returns string with pageTitle replaced
 */
function replaceTranslationKeysWithText(body, generator, regex) {
  let match;

  // eslint-disable-next-line no-cond-assign
  while ((match = regex.exec(body)) !== null) {
    // match is now the next match, in array form and our key is at index 1, index 1 is replace target.
    const key = match[1];
    const target = key;
    const jsonData = geti18nJson(key, generator);
    const keyValue = jsonData !== undefined ? deepFind(jsonData, key) : undefined;

    body = body.replace(target, keyValue !== undefined ? keyValue : generator.baseName);
  }

  return body;
}

/**
 *
 * @param {string} body html body
 * @param {object} generator reference to the generator
 * @returns string with placeholders replaced
 */
function replacePlaceholders(body, generator) {
  const re = /placeholder=['|"]([{]{2}\s*['|"]([a-zA-Z0-9.\-_]+)['|"][\s][|][\s](translate)\s*[}]{2})['|"]/g;
  let match;

  // eslint-disable-next-line no-cond-assign
  while ((match = re.exec(body)) !== null) {
    // match is now the next match, in array form and our key is at index 2, index 1 is replace target.
    const key = match[2];
    const target = match[1];
    const jsonData = geti18nJson(key, generator);
    const keyValue = jsonData !== undefined ? deepFind(jsonData, key, true) : undefined; // dirty fix to get placeholder as it is not in proper json format, name has a dot in it. Assuming that all placeholders are in similar format

    body = body.replace(target, keyValue !== undefined ? keyValue : '');
  }

  return body;
}

/**
 *
 * @param {string} body html body
 * @param {object} generator reference to the generator
 * @returns string with Translate components replaced
 */
function replaceTranslation(body, generator) {
  const replaceRegex = (re, defultReplaceText) => {
    let match;
    // eslint-disable-next-line no-cond-assign
    while ((match = re.exec(body)) !== null) {
      // match is now the next match, in array form and our key is at index 2, index 1 is replace target.
      const key = match[2];
      const target = match[1];
      const limit = match[4]; // string indicating validation limit (e.g. "{ max: 4 }")
      const jsonData = geti18nJson(key, generator);
      let keyValue = jsonData !== undefined ? deepFind(jsonData, key) : undefined;
      if (!keyValue) {
        keyValue = deepFind(jsonData, key, true); // dirty fix to get placeholder as it is not in proper json format, name has a dot in it. Assuming that all placeholders are in similar format
      }
      if (limit) {
        // Replace "{{ placeholder }}" with numeric limit
        keyValue = keyValue.replace(/{{.+}}/, /{.+:\s(.+)\s}/.exec(limit)[1]);
      }

      body = body.replace(target, keyValue !== undefined ? `"${keyValue}"` : defultReplaceText);
    }
  };

  replaceRegex(/(\{translate\('([a-zA-Z0-9.\-_]+)'(, ?null, ?'.*')?\)\})/g, '""');
  replaceRegex(/(translate\(\s*'([a-zA-Z0-9.\-_]+)'(,\s*(null|\{.*\}),?\s*('.*')?\s*)?\))/g, "''");

  return body;
}

/**
 *
 * @param key i18n key
 * @param {object} generator reference to the generator
 * @returns parsed json file
 */
function geti18nJson(key, generator) {
  const i18nDirectory = `${LANGUAGES_MAIN_SRC_DIR}i18n/en/`;
  const names = [];
  let result;
  const namePrefix = _.kebabCase(key.split('.')[0]);
  if (['entity', 'error', 'footer'].includes(namePrefix)) {
    names.push('global');
    if (namePrefix === 'error') {
      names.push('error');
    }
  } else {
    names.push(namePrefix);
  }
  for (let i = 0; i < names.length; i++) {
    let filename = `${i18nDirectory + names[i]}.json`;
    let render;
    if (!shelljs.test('-f', filename)) {
      filename = `${filename}.ejs`;
      render = true;
    }
    try {
      let file = generator.fs.read(filename);
      file = render ? ejs.render(file, generator, {}) : file;
      file = JSON.parse(file);
      if (result === undefined) {
        result = file;
      } else {
        result = { ...result, ...file };
      }
    } catch (err) {
      generator.log(err);
      generator.log(`Error in file: ${filename}`);
    }
  }
  return result;
}

/**
 *
 * @param obj object to find in
 * @param path path to traverse
 * @param placeholder placeholder
 */
function deepFind(obj, path, placeholder) {
  const paths = path.split('.');
  let current = obj;
  if (placeholder) {
    // dirty fix for placeholders, the json files needs to be corrected
    paths[paths.length - 2] = `${paths[paths.length - 2]}.${paths[paths.length - 1]}`;
    paths.pop();
  }
  for (let i = 0; i < paths.length; ++i) {
    if (current[paths[i]] === undefined) {
      return undefined;
    }
    current = current[paths[i]];
  }
  return current;
}

/**
 * Convert passed block of string to javadoc formatted string.
 *
 * @param {string} text text to convert to javadoc format
 * @param {number} indentSize indent size (default 0)
 * @returns javadoc formatted string
 */
function getJavadoc(text, indentSize = 0) {
  if (!text) {
    text = '';
  }
  if (text.includes('"')) {
    text = text.replace(/"/g, '\\"');
  }
  let javadoc = `${_.repeat(' ', indentSize)}/**`;
  const rows = text.split('\n');
  for (let i = 0; i < rows.length; i++) {
    javadoc = `${javadoc}\n${_.repeat(' ', indentSize)} * ${rows[i]}`;
  }
  javadoc = `${javadoc}\n${_.repeat(' ', indentSize)} */`;
  return javadoc;
}

/**
 * Build an enum object
 * @param {Object} field - entity field
 * @param {String} clientRootFolder - the client's root folder
 * @return {Object} the enum info.
 */
function getEnumInfo(field, clientRootFolder) {
  const fieldType = field.fieldType;
  // Todo: check if the next line does a side-effect and refactor it.
  field.enumInstance = _.lowerFirst(fieldType);
  const enums = field.fieldValues.split(',').map(fieldValue => fieldValue.trim());
  const customValuesState = getCustomValuesState(enums);
  return {
    enumName: fieldType,
    javadoc: field.fieldTypeJavadoc && getJavadoc(field.fieldTypeJavadoc),
    enumInstance: field.enumInstance,
    enums,
    ...customValuesState,
    enumValues: getEnums(enums, customValuesState, field.fieldValuesJavadocs),
    clientRootFolder: clientRootFolder ? `${clientRootFolder}-` : '',
  };
}

/**
 * @Deprecated
 * Build an enum object, deprecated use getEnumInfoInstead
 * @param {any} field : entity field
 * @param {string} frontendAppName
 * @param {string} packageName
 * @param {string} clientRootFolder
 */
function buildEnumInfo(field, frontendAppName, packageName, clientRootFolder) {
  const fieldType = field.fieldType;
  field.enumInstance = _.lowerFirst(fieldType);
  const enums = field.fieldValues.replace(/\s/g, '').split(',');
  const enumsWithCustomValue = getEnumsWithCustomValue(enums);
  return {
    enumName: fieldType,
    enumValues: field.fieldValues.split(',').join(', '),
    enumInstance: field.enumInstance,
    enums,
    enumsWithCustomValue,
    frontendAppName,
    packageName,
    clientRootFolder: clientRootFolder ? `${clientRootFolder}-` : '',
  };
}

/**
 * @deprecated
 * private function to remove for jhipster v7
 * @param enums
 * @return {*}
 */
function getEnumsWithCustomValue(enums) {
  return enums.reduce((enumsWithCustomValueArray, currentEnumValue) => {
    if (doesTheEnumValueHaveACustomValue(currentEnumValue)) {
      const matches = /([A-Z\-_]+)(\((.+?)\))?/.exec(currentEnumValue);
      const enumValueName = matches[1];
      const enumValueCustomValue = matches[3];
      enumsWithCustomValueArray.push({ name: enumValueName, value: enumValueCustomValue });
    } else {
      enumsWithCustomValueArray.push({ name: currentEnumValue, value: false });
    }
    return enumsWithCustomValueArray;
  }, []);
}

function getCustomValuesState(enumValues) {
  const state = {
    withoutCustomValue: 0,
    withCustomValue: 0,
  };
  enumValues.forEach(enumValue => {
    if (doesTheEnumValueHaveACustomValue(enumValue)) {
      state.withCustomValue++;
    } else {
      state.withoutCustomValue++;
    }
  });
  return {
    withoutCustomValues: state.withCustomValue === 0,
    withSomeCustomValues: state.withCustomValue !== 0 && state.withoutCustomValue !== 0,
    withCustomValues: state.withoutCustomValue === 0,
  };
}

function getEnums(enums, customValuesState, comments) {
  if (customValuesState.withoutCustomValues) {
    return enums.map(enumValue => ({
      name: enumValue,
      value: enumValue,
      comment: comments && comments[enumValue] && getJavadoc(comments[enumValue], 4),
    }));
  }
  return enums.map(enumValue => {
    if (!doesTheEnumValueHaveACustomValue(enumValue)) {
      return {
        name: enumValue.trim(),
        value: enumValue.trim(),
        comment: comments && comments[enumValue] && getJavadoc(comments[enumValue], 4),
      };
    }
    // eslint-disable-next-line no-unused-vars
    const matched = /\s*(.+?)\s*\((.+?)\)/.exec(enumValue);
    return {
      name: matched[1],
      value: matched[2],
      comment: comments && comments[matched[1]] && getJavadoc(comments[matched[1]], 4),
    };
  });
}

function doesTheEnumValueHaveACustomValue(enumValue) {
  return enumValue.includes('(');
}
/**
 * Copy object props from source to destination
 * @param {*} toObj
 * @param {*} fromObj
 */
function copyObjectProps(toObj, fromObj) {
  // we use Object.assign instead of spread as we want to mutilate the object.
  Object.assign(toObj, fromObj);
}

/**
 * Decode the given string from base64 to said encoding.
 * @param string the base64 string to decode
 * @param encoding the encoding to decode into. default to 'utf-8'
 */
function decodeBase64(string, encoding = 'utf-8') {
  return Buffer.from(string, 'base64').toString(encoding);
}

function loadYoRc(filePath = '.yo-rc.json') {
  if (!FileUtils.doesFileExist(filePath)) {
    return undefined;
  }
  return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf-8' }));
}

/**
 * Get DB type from DB value
 * @param {string} db - db
 */
function getDBTypeFromDBValue(db) {
  if (constants.SQL_DB_OPTIONS.map(db => db.value).includes(db)) {
    return SQL;
  }
  return db;
}

/**
 * Get a random hex string
 * @param {int} len the length to use, defaults to 50
 */
function getRandomHex(len = 50) {
  return crypto.randomBytes(len).toString('hex');
}

/**
 * Generates a base64 secret from given string or random hex
 * @param {string} value the value used to get base64 secret
 * @param {int} len the length to use for random hex, defaults to 50
 */
function getBase64Secret(value = '', len = 50) {
  if (this && this.options && this.options.reproducibleTests) {
    if (value) {
      return `SECRET-${value}-${len}`;
    }
    return `SECRET--${len}`;
  }
  return Buffer.from(value || getRandomHex(len)).toString('base64');
}

/**
 * Checks if string is already in file
 * @param {string} path file path
 * @param {string} search search string
 * @param {object} generator reference to generator
 * @returns {boolean} true if string is in file, false otherwise
 */
function checkStringInFile(path, search, generator) {
  const fileContent = generator.fs.read(generator.destinationPath(path));
  return fileContent.includes(search);
}

/**
 * Checks if regex is found in file
 * @param {string} path file path
 * @param {regex} regex regular expression
 * @param {object} generator reference to generator
 * @returns {boolean} true if regex is matched in file, false otherwise
 */
function checkRegexInFile(path, regex, generator) {
  const fileContent = generator.fs.read(generator.destinationPath(path));
  return fileContent.match(regex);
}

/**
 * Remove 'generator-' prefix from generators for compatibility with yeoman namespaces.
 * @param {string} packageName - name of the blueprint's package name
 * @returns {string} namespace of the blueprint
 */
function packageNameToNamespace(packageName) {
  return packageName.replace('generator-', '');
}

/**
 * Calculate a hash code for a given string.
 * @param {string} str - any string
 * @returns {number} returns the calculated hash code.
 */
function stringHashCode(str) {
  let hash = 0;

  for (let i = 0; i < str.length; i++) {
    const character = str.charCodeAt(i);
    hash = (hash << 5) - hash + character; // eslint-disable-line no-bitwise
    hash |= 0; // eslint-disable-line no-bitwise
  }

  if (hash < 0) {
    hash *= -1;
  }
  return hash;
}

/**
 * Executes a Git command using shellJS
 * gitExec(args [, options, callback])
 *
 * @param {string|array} args - can be an array of arguments or a string command
 * @param {object} options[optional] - takes any of child process options
 * @param {function} callback[optional] - a callback function to be called once process complete, The call back will receive code, stdout and stderr
 * @return {object} when in synchronous mode, this returns a ShellString. Otherwise, this returns the child process object.
 */
function gitExec(args, options = {}, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = {};
  }

  if (options.async === undefined) options.async = callback !== undefined;
  if (options.silent === undefined) options.silent = true;
  if (options.trace === undefined) options.trace = true;

  if (!Array.isArray(args)) {
    args = [args];
  }
  const command = `git ${args.join(' ')}`;
  if (options.trace) {
    console.info(command);
  }
  if (callback) {
    return shelljs.exec(command, options, callback);
  }
  return shelljs.exec(command, options);
}

/**
 * Checks if git is installed.
 *
 * @param {function} callback[optional] - function to be called after checking if git is installed. The callback will receive the code of the shell command executed.
 *
 * @return {boolean} true if installed; false otherwise..
 */
function isGitInstalled(callback) {
  const code = gitExec('--version', { trace: false }).code;
  if (callback) callback(code);
  return code === 0;
}

/**
 * Replace translation for Vue application
 * @param {*} generator
 * @param {*} files
 */
function vueReplaceTranslation(generator, files) {
  for (let i = 0; i < files.length; i++) {
    const filePath = `${generator.CLIENT_MAIN_SRC_DIR}${files[i]}`;
    // Match the below attributes and the $t() method
    const regexp = ['v-text', 'v-bind:placeholder', 'v-html', 'v-bind:title', 'v-bind:label', 'v-bind:value', 'v-bind:html']
      .map(s => `${s}="\\$t\\(.*?\\)"`)
      .join(')|(');
    replaceContent(
      {
        file: filePath,
        pattern: new RegExp(` ?(${regexp})`, 'g'),
        content: '',
      },
      generator
    );
  }
}

function vueAddPageToRouterImport(generator, pageName, pageFolderName, pageFilename = pageFolderName) {
  rewriteFile(
    {
      file: `${generator.CLIENT_MAIN_SRC_DIR}/app/router/pages.ts`,
      needle: 'jhipster-needle-add-entity-to-router-import',
      splicable: [
        generator.stripMargin(
          // prettier-ignore
          `|// prettier-ignore
                |const ${pageName} = () => import('@/pages/${pageFolderName}/${pageFilename}.vue');`
        ),
      ],
    },
    generator
  );
}

function vueAddPageToRouter(generator, pageName, pageFilename) {
  rewriteFile(
    {
      file: `${generator.CLIENT_MAIN_SRC_DIR}/app/router/pages.ts`,
      needle: 'jhipster-needle-add-entity-to-router',
      splicable: [
        generator.stripMargin(
          // prettier-ignore
          `|{
                    |    path: '/pages/${pageFilename}',
                    |    name: '${pageName}',
                    |    component: ${pageName},
                    |    meta: { authorities: [Authority.USER] }
                    |  },`
        ),
      ],
    },
    generator
  );
}

function vueAddPageServiceToMainImport(generator, pageName, pageFolderName, pageFilename = pageFolderName) {
  rewriteFile(
    {
      file: `${generator.CLIENT_MAIN_SRC_DIR}/app/main.ts`,
      needle: 'jhipster-needle-add-entity-service-to-main-import',
      splicable: [
        generator.stripMargin(
          // prettier-ignore
          `|import ${pageName}Service from '@/pages/${pageFolderName}/${pageFilename}.service';`
        ),
      ],
    },
    generator
  );
}

function vueAddPageServiceToMain(generator, pageName, pageInstance) {
  rewriteFile(
    {
      file: `${generator.CLIENT_MAIN_SRC_DIR}/app/main.ts`,
      needle: 'jhipster-needle-add-entity-service-to-main',
      splicable: [
        generator.stripMargin(
          // prettier-ignore
          `|${pageInstance}Service: () => new ${pageName}Service(),`
        ),
      ],
    },
    generator
  );
}

function vueAddPageProtractorConf(generator) {
  rewriteFile(
    {
      file: `${generator.CLIENT_TEST_SRC_DIR}/protractor.conf.js`,
      needle: 'jhipster-needle-add-protractor-tests',
      splicable: [generator.stripMargin("'./e2e/pages/**/*.spec.ts',")],
    },
    generator
  );
}

function languageSnakeCase(language) {
  // Template the message server side properties
  return language.replace(/-/g, '_');
}

function languageToJavaLanguage(language) {
  // Template the message server side properties
  const langProp = languageSnakeCase(language);
  // Target file : change xx_yyyy_zz to xx_yyyy_ZZ to match java locales
  return langProp.replace(/_[a-z]+$/g, lang => lang.toUpperCase());
}

/**
 * @private
 * Utility function add condition to every block in addition to the already existing condition.
 */
function addSectionsCondition(files, commonCondition) {
  return Object.fromEntries(
    Object.entries(files).map(([sectionName, sectionValue]) => {
      sectionValue = sectionValue.map(block => {
        const { condition } = block;
        let newCondition = commonCondition;
        if (typeof condition === 'function') {
          newCondition = (...args) => {
            return commonCondition(...args) && condition(...args);
          };
        } else if (condition !== undefined) {
          newCondition = (...args) => commonCondition(...args) && condition;
        }
        block = {
          ...block,
          condition: newCondition,
        };
        return block;
      });
      return [sectionName, sectionValue];
    })
  );
}

/**
 * @private
 * Utility function to merge sections (jhipster files structure)
 * Merging { foo: [blocks1], bar: [block2]} and { foo: [blocks3], bar: [block4]}
 * Results in { foo: [blocks1, block3], bar: [block2, block4]}
 */
function mergeSections(...allFiles) {
  const generated = {};
  for (const files of allFiles) {
    for (const [sectionName, sectionValue] of Object.entries(files)) {
      generated[sectionName] = generated[sectionName] || [];
      generated[sectionName].push(...sectionValue);
    }
  }
  return generated;
}
