// @flow

import type {ConfigResult, File, FilePath} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
import ThrowableDiagnostic from '@parcel/diagnostic';
import path from 'path';
import clone from 'clone';
import json5 from 'json5';
import {parse as toml} from '@iarna/toml';
import {LRUCache} from 'lru-cache';

export type ConfigOutput = {|
  config: ConfigResult,
  files: Array<File>,
|};

export type ConfigOptions = {|
  parse?: boolean,
  parser?: string => any,
|};

const configCache = new LRUCache<FilePath, ConfigOutput>({max: 500});
const resolveCache = new Map();

export function resolveConfig(
  fs: FileSystem,
  filepath: FilePath,
  filenames: Array<FilePath>,
  projectRoot: FilePath,
): Promise<?FilePath> {
  // Cache the result of resolving config for this directory.
  // This is automatically invalidated at the end of the current build.
  let key = path.dirname(filepath) + filenames.join(',');
  let cached = resolveCache.get(key);
  if (cached !== undefined) {
    return Promise.resolve(cached);
  }

  let resolved = fs.findAncestorFile(
    filenames,
    path.dirname(filepath),
    projectRoot,
  );
  resolveCache.set(key, resolved);
  return Promise.resolve(resolved);
}

export function resolveConfigSync(
  fs: FileSystem,
  filepath: FilePath,
  filenames: Array<FilePath>,
  projectRoot: FilePath,
): ?FilePath {
  return fs.findAncestorFile(filenames, path.dirname(filepath), projectRoot);
}

export async function loadConfig(
  fs: FileSystem,
  filepath: FilePath,
  filenames: Array<FilePath>,
  projectRoot: FilePath,
  opts: ?ConfigOptions,
): Promise<ConfigOutput | null> {
  let parse = opts?.parse ?? true;
  let configFile = await resolveConfig(fs, filepath, filenames, projectRoot);
  if (configFile) {
    let cachedOutput = configCache.get(String(parse) + configFile);
    if (cachedOutput) {
      return cachedOutput;
    }

    try {
      let extname = path.extname(configFile).slice(1);
      if (extname === 'js' || extname === 'cjs') {
        let output = {
          // $FlowFixMe
          config: clone(module.require(configFile)),
          files: [{filePath: configFile}],
        };

        configCache.set(configFile, output);
        return output;
      }

      return readConfig(fs, configFile, opts);
    } catch (err) {
      if (err.code === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
        return null;
      }

      throw err;
    }
  }

  return null;
}

loadConfig.clear = () => {
  configCache.clear();
  resolveCache.clear();
};

export async function readConfig(
  fs: FileSystem,
  configFile: FilePath,
  opts: ?ConfigOptions,
): Promise<ConfigOutput | null> {
  let parse = opts?.parse ?? true;
  let cachedOutput = configCache.get(String(parse) + configFile);
  if (cachedOutput) {
    return cachedOutput;
  }

  try {
    let configContent = await fs.readFile(configFile, 'utf8');
    let config;
    if (parse === false) {
      config = configContent;
    } else {
      let extname = path.extname(configFile).slice(1);
      let parse = opts?.parser ?? getParser(extname);
      try {
        config = parse(configContent);
      } catch (e) {
        if (extname !== '' && extname !== 'json') {
          throw e;
        }

        let pos = {
          line: e.lineNumber,
          column: e.columnNumber,
        };

        throw new ThrowableDiagnostic({
          diagnostic: {
            message: `Failed to parse ${path.basename(configFile)}`,
            origin: '@parcel/utils',
            codeFrames: [
              {
                language: 'json5',
                filePath: configFile,
                code: configContent,
                codeHighlights: [
                  {
                    start: pos,
                    end: pos,
                    message: e.message,
                  },
                ],
              },
            ],
          },
        });
      }
    }

    let output = {
      config,
      files: [{filePath: configFile}],
    };

    configCache.set(String(parse) + configFile, output);
    return output;
  } catch (err) {
    if (err.code === 'MODULE_NOT_FOUND' || err.code === 'ENOENT') {
      return null;
    }

    throw err;
  }
}

function getParser(extname) {
  switch (extname) {
    case 'toml':
      return toml;
    case 'json':
    default:
      return json5.parse;
  }
}
