import { existsSync, path, readFileSync } from "./file-methods.ts";
const _BOM = /^\uFEFF/;

// express is set like: app.engine('html', require('eta').renderFile)

import EtaErr from "./err.ts";

/* TYPES */

import type { EtaConfig } from "./config.ts";

/* END TYPES */

/**
 * Get the path to the included file from the parent file path and the
 * specified path.
 *
 * If `name` does not have an extension, it will default to `.eta`
 *
 * @param name specified path
 * @param parentfile parent file path
 * @param isDirectory whether parentfile is a directory
 * @return absolute path to template
 */

function getWholeFilePath(
  name: string,
  parentfile: string,
  isDirectory?: boolean,
): string {
  const includePath = path.resolve(
    isDirectory ? parentfile : path.dirname(parentfile), // returns directory the parent file is in
    name, // file
  ) + (path.extname(name) ? "" : ".eta");
  return includePath;
}

/**
 * Get the absolute path to an included template
 *
 * If this is called with an absolute path (for example, starting with '/' or 'C:\')
 * then Eta will attempt to resolve the absolute path within options.views. If it cannot,
 * Eta will fallback to options.root or '/'
 *
 * If this is called with a relative path, Eta will:
 * - Look relative to the current template (if the current template has the `filename` property)
 * - Look inside each directory in options.views
 *
 * Note: if Eta is unable to find a template using path and options, it will throw an error.
 *
 * @param path    specified path
 * @param options compilation options
 * @return absolute path to template
 */

function getPath(path: string, options: EtaConfig): string {
  let includePath: string | false = false;
  const views = options.views;
  let searchedPaths: Array<string> = [];

  // If these four values are the same,
  // getPath() will return the same result every time.
  // We can cache the result to avoid expensive
  // file operations.
  const pathOptions = JSON.stringify({
    filename: options.filename, // filename of the template which called includeFile()
    path: path,
    root: options.root,
    views: options.views,
  });

  if (
    options.cache && options.filepathCache && options.filepathCache[pathOptions]
  ) {
    // Use the cached filepath
    return options.filepathCache[pathOptions];
  }

  /** Add a filepath to the list of paths we've checked for a template */
  function addPathToSearched(pathSearched: string) {
    if (!searchedPaths.includes(pathSearched)) {
      searchedPaths.push(pathSearched);
    }
  }

  /**
   * Take a filepath (like 'partials/mypartial.eta'). Attempt to find the template file inside `views`;
   * return the resulting template file path, or `false` to indicate that the template was not found.
   *
   * @param views the filepath that holds templates, or an array of filepaths that hold templates
   * @param path the path to the template
   */

  function searchViews(
    views: Array<string> | string | undefined,
    path: string,
  ): string | false {
    let filePath;

    // If views is an array, then loop through each directory
    // And attempt to find the template
    if (
      Array.isArray(views) &&
      views.some(function (v) {
        filePath = getWholeFilePath(path, v, true);

        addPathToSearched(filePath);

        return existsSync(filePath);
      })
    ) {
      // If the above returned true, we know that the filePath was just set to a path
      // That exists (Array.some() returns as soon as it finds a valid element)
      return (filePath as unknown) as string;
    } else if (typeof views === "string") {
      // Search for the file if views is a single directory
      filePath = getWholeFilePath(path, views, true);

      addPathToSearched(filePath);

      if (existsSync(filePath)) {
        return filePath;
      }
    }

    // Unable to find a file
    return false;
  }

  // Path starts with '/', 'C:\', etc.
  const match = /^[A-Za-z]+:\\|^\//.exec(path);

  // Absolute path, like /partials/partial.eta
  if (match && match.length) {
    // We have to trim the beginning '/' off the path, or else
    // path.resolve(dir, path) will always resolve to just path
    const formattedPath = path.replace(/^\/*/, "");

    // First, try to resolve the path within options.views
    includePath = searchViews(views, formattedPath);
    if (!includePath) {
      // If that fails, searchViews will return false. Try to find the path
      // inside options.root (by default '/', the base of the filesystem)
      const pathFromRoot = getWholeFilePath(
        formattedPath,
        options.root || "/",
        true,
      );

      addPathToSearched(pathFromRoot);

      includePath = pathFromRoot;
    }
  } else {
    // Relative paths
    // Look relative to a passed filename first
    if (options.filename) {
      const filePath = getWholeFilePath(path, options.filename);

      addPathToSearched(filePath);

      if (existsSync(filePath)) {
        includePath = filePath;
      }
    }
    // Then look for the template in options.views
    if (!includePath) {
      includePath = searchViews(views, path);
    }
    if (!includePath) {
      throw EtaErr(
        'Could not find the template "' + path + '". Paths tried: ' +
          searchedPaths,
      );
    }
  }

  // If caching and filepathCache are enabled,
  // cache the input & output of this function.
  if (options.cache && options.filepathCache) {
    options.filepathCache[pathOptions] = includePath;
  }

  return includePath;
}

/**
 * Reads a file synchronously
 */

function readFile(filePath: string): string {
  try {
    return readFileSync(filePath).toString().replace(_BOM, "") // TODO: is replacing BOM's necessary?
    ;
  } catch {
    throw EtaErr("Failed to read template at '" + filePath + "'");
  }
}

export { getPath, readFile };