const path = require("path");
const fs = require("fs");

function TemplatePath() {}

/**
 * @returns {String} the absolute path to Eleventy’s project directory.
 */
TemplatePath.getWorkingDir = function () {
  return TemplatePath.normalize(path.resolve("."));
};

/**
 * Returns the directory portion of a path.
 * Works for directory and file paths and paths ending in a glob pattern.
 *
 * @param {String} path - A path
 * @returns {String} the directory portion of a path.
 */
TemplatePath.getDir = function (path) {
  if (TemplatePath.isDirectorySync(path)) {
    return path;
  }

  return TemplatePath.getDirFromFilePath(path);
};

/**
 * Returns the directory portion of a path that either points to a file
 * or ends in a glob pattern. If `path` points to a directory,
 * the returned value will have its last path segment stripped
 * due to how [`path.parse`][1] works.
 *
 * [1]: https://nodejs.org/api/path.html#path_path_parse_path
 *
 * @returns {String} the directory portion of a path.
 * @param {String} filePath - A path
 */
TemplatePath.getDirFromFilePath = function (filePath) {
  return path.parse(filePath).dir || ".";
};

/**
 * Returns the last path segment in a path (no leading/trailing slashes).
 *
 * Assumes [`path.parse`][1] was called on `path` before.
 *
 * [1]: https://nodejs.org/api/path.html#path_path_parse_path
 *
 * @param {String} path - A path
 * @returns {String} the last path segment in a path
 */
TemplatePath.getLastPathSegment = function (path) {
  if (!path.includes("/")) {
    return path;
  }

  // Trim a trailing slash if there is one
  path = path.replace(/\/$/, "");

  return path.slice(path.lastIndexOf("/") + 1);
};

/**
 * @param {String} path - A path
 * @returns {String[]} an array of paths pointing to each path segment of the
 * provided `path`.
 */
TemplatePath.getAllDirs = function (path) {
  // Trim a trailing slash if there is one
  path = path.replace(/\/$/, "");

  if (!path.includes("/")) {
    return [path];
  }

  return path
    .split("/")
    .map((segment, index, array) => array.slice(0, index + 1).join("/"))
    .filter((path) => path !== ".")
    .reverse();
};

/**
 * Normalizes a path, resolving single-dot and double-dot segments.
 *
 * Node.js’ [`path.normalize`][1] is called to strip a possible leading `"./"` segment.
 *
 * [1]: https://nodejs.org/api/path.html#path_path_normalize_path
 *
 * @param {String} thePath - The path that should be normalized.
 * @returns {String} the normalized path.
 */
TemplatePath.normalize = function (thePath) {
  let filePath = path.normalize(thePath).split(path.sep).join("/");
  if(filePath !== "/" && filePath.endsWith("/")) {
    return filePath.slice(0, -1);
  }
  return filePath;
};

/**
 * Joins all given path segments together.
 *
 * It uses Node.js’ [`path.join`][1] method.
 *
 * [1]: https://nodejs.org/api/path.html#path_path_join_paths
 *
 * @param {...String} paths - An arbitrary amount of path segments.
 * @returns {String} the normalized and joined path.
 */
TemplatePath.join = function (...paths) {
  return TemplatePath.normalize(path.join(...paths));
};

/**
 * Joins the given URL path segments and normalizes the resulting path.
 * Maintains a single trailing slash if the last URL path argument
 * had at least one.
 *
 * @param {...String} urlPaths
 * @returns {String} a normalized URL path described by the given URL path segments.
 */
TemplatePath.normalizeUrlPath = function (...urlPaths) {
  const urlPath = path.posix.join(...urlPaths);
  return urlPath.replace(/\/+$/, "/");
};

/**
 * Joins the given path segments. Since the first path is absolute,
 * the resulting path will be absolute as well.
 *
 * @param {...String} paths
 * @returns {String} the absolute path described by the given path segments.
 */
TemplatePath.absolutePath = function (...paths) {
  let i = 0;
  // check all the paths before we short circuit from the first index
  for (let p of paths) {
    if (path.isAbsolute(p) && i > 0) {
      throw new Error(
        `Only the first parameter to Template.absolutePath can be an absolute path. Received: ${p} from ${paths}`
      );
    }
    i++;
  }

  let j = 0;
  for (let p of paths) {
    if (j === 0 && path.isAbsolute(p)) {
      return TemplatePath.join(...paths);
    }
    j++;
  }

  return TemplatePath.join(TemplatePath.getWorkingDir(), ...paths);
};

/**
 * Turns an absolute path into a path relative to the project directory.
 *
 * @param {String} absolutePath
 * @returns {String} the relative path.
 */
TemplatePath.relativePath = function (absolutePath) {
  return TemplatePath.stripLeadingSubPath(
    absolutePath,
    TemplatePath.getWorkingDir()
  );
};

/**
 * Adds a leading dot-slash segment to each path in the `paths` array.
 *
 * @param {String[]} paths
 * @returns {String[]}
 */
TemplatePath.addLeadingDotSlashArray = function (paths) {
  return paths.map((path) => TemplatePath.addLeadingDotSlash(path));
};

/**
 * Adds a leading dot-slash segment to `path`.
 *
 * @param {String} pathArg
 * @returns {String}
 */
TemplatePath.addLeadingDotSlash = function (pathArg) {
  if (pathArg === "." || pathArg === "..") {
    return pathArg + "/";
  }

  if (
    path.isAbsolute(pathArg) ||
    pathArg.startsWith("./") ||
    pathArg.startsWith("../")
  ) {
    return pathArg;
  }

  return "./" + pathArg;
};

/**
 * Removes a leading dot-slash segment.
 *
 * @param {String} path
 * @returns {String} the `path` without a leading dot-slash segment.
 */
TemplatePath.stripLeadingDotSlash = function (path) {
  return typeof path === "string" ? path.replace(/^\.\//, "") : path;
};

/**
 * Determines whether a path starts with a given sub path.
 *
 * @param {String} path - A path
 * @param {String} subPath - A path
 * @returns {Boolean} whether `path` starts with `subPath`.
 */
TemplatePath.startsWithSubPath = function (path, subPath) {
  path = TemplatePath.normalize(path);
  subPath = TemplatePath.normalize(subPath);

  return path.startsWith(subPath);
};

/**
 * Removes the `subPath` at the start of `path` if present
 * and returns the remainding path.
 *
 * @param {String} path - A path
 * @param {String} subPath - A path
 * @returns {String} the `path` without `subPath` at the start of it.
 */
TemplatePath.stripLeadingSubPath = function (path, subPath) {
  path = TemplatePath.normalize(path);
  subPath = TemplatePath.normalize(subPath);

  if (subPath !== "." && path.startsWith(subPath)) {
    return path.slice(subPath.length + 1);
  }

  return path;
};

/**
 * @param {String} path - A path
 * @returns {Boolean} whether `path` points to an existing directory.
 */
TemplatePath.isDirectorySync = function (path) {
  return fs.existsSync(path) && fs.statSync(path).isDirectory();
};

/**
 * @param {String} path - A path
 * @returns {Boolean} whether `path` points to an existing directory.
 */
TemplatePath.isDirectory = async function (path) {
  return new Promise((resolve) => {
    fs.stat(path, (err, stats) => {
      if (stats) {
        resolve(stats.isDirectory());
      }
      resolve(false);
    });
  });
};

/**
 * Appends a recursive wildcard glob pattern to `path`
 * unless `path` is not a directory; then, `path` is assumed to be a file path
 * and is left unchaged.
 *
 * @param {String} path
 * @returns {String}
 */
TemplatePath.convertToRecursiveGlobSync = function (path) {
  if (path === "") {
    return "./**";
  }

  path = TemplatePath.addLeadingDotSlash(path);

  if (TemplatePath.isDirectorySync(path)) {
    return path + (!path.endsWith("/") ? "/" : "") + "**";
  }

  return path;
};

/**
 * Appends a recursive wildcard glob pattern to `path`
 * unless `path` is not a directory; then, `path` is assumed to be a file path
 * and is left unchaged.
 *
 * @param {String} path
 * @returns {String}
 */
TemplatePath.convertToRecursiveGlob = async function (path) {
  if (path === "") {
    return "./**";
  }

  path = TemplatePath.addLeadingDotSlash(path);

  if (await TemplatePath.isDirectory(path)) {
    return path + (!path.endsWith("/") ? "/" : "") + "**";
  }

  return path;
};

/**
 * Returns the extension of the path without the leading dot.
 * If the path has no extensions, the empty string is returned.
 *
 * @param {String} thePath
 * @returns {String} the path’s extension if it exists;
 * otherwise, the empty string.
 */
TemplatePath.getExtension = function (thePath) {
  return path.extname(thePath).replace(/^\./, "");
};

/**
 * Removes the extension from a path.
 *
 * @param {String} path
 * @param {String} [extension]
 * @returns {String}
 */
TemplatePath.removeExtension = function (path, extension = undefined) {
  if (extension === undefined) {
    return path;
  }

  const pathExtension = TemplatePath.getExtension(path);
  if (pathExtension !== "" && extension.endsWith(pathExtension)) {
    return path.substring(0, path.lastIndexOf(pathExtension) - 1);
  }

  return path;
};

/**
 * Accepts a relative file path that is using a standard directory separator and
 * normalizes it using the local operating system separator.
 * e.g. `./my/dir/` stays `./my/dir/` on *nix and becomes `.\\my\\dir\\` on Windows
 *
 * @param {String} filePath
 * @param {String} [sep="/"]
 * @returns {String} a file path with the correct local directory separator.
 */
TemplatePath.normalizeOperatingSystemFilePath = function (filePath, sep = "/") {
  return filePath.split(sep).join(path.sep);
};

/**
 * Accepts a relative file path with the local operating system directory separator and
 * normalizes it using a forward slash directory separator. (Leaves trailing slash as-is)
 * e.g. `./my/dir/` stays `./my/dir/` on *nix
 * e.g. `.\\my\\dir\\` becomes `./my/dir/` on *nix and Windows
 *
 * @param {String} filePath
 * @param {String} [sep="/"]
 * @returns {String} a file path with the correct local directory separator.
 */
TemplatePath.standardizeFilePath = function (filePath, sep = "/") {
  return TemplatePath.addLeadingDotSlash(filePath.split(path.sep).join(sep));
};

module.exports = TemplatePath;
