// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>

#include "gn/err.h"
#include "gn/filesystem_utils.h"
#include "gn/functions.h"
#include "gn/parse_tree.h"
#include "gn/scope.h"
#include "gn/value.h"

namespace functions {

namespace {

// Corresponds to the various values of "what" in the function call.
enum What {
  WHAT_FILE,
  WHAT_NAME,
  WHAT_EXTENSION,
  WHAT_DIR,
  WHAT_ABSPATH,
  WHAT_GEN_DIR,
  WHAT_OUT_DIR,
};

// Returns the directory containing the input (resolving it against the
// |current_dir|), regardless of whether the input is a directory or a file.
SourceDir DirForInput(const Settings* settings,
                      const SourceDir& current_dir,
                      const Value& input,
                      Err* err) {
  // Input should already have been validated as a string.
  const std::string& input_string = input.string_value();

  if (!input_string.empty() && input_string[input_string.size() - 1] == '/') {
    // Input is a directory.
    return current_dir.ResolveRelativeDir(
        input, err, settings->build_settings()->root_path_utf8());
  }

  // Input is a file.
  return current_dir
      .ResolveRelativeFile(input, err,
                           settings->build_settings()->root_path_utf8())
      .GetDir();
}

std::string GetOnePathInfo(const Settings* settings,
                           const SourceDir& current_dir,
                           What what,
                           const Value& input,
                           Err* err) {
  if (!input.VerifyTypeIs(Value::STRING, err))
    return std::string();
  const std::string& input_string = input.string_value();
  if (input_string.empty()) {
    *err = Err(input, "Calling get_path_info on an empty string.");
    return std::string();
  }

  switch (what) {
    case WHAT_FILE: {
      return std::string(FindFilename(&input_string));
    }
    case WHAT_NAME: {
      std::string file(FindFilename(&input_string));
      size_t extension_offset = FindExtensionOffset(file);
      if (extension_offset == std::string::npos)
        return file;
      // Trim extension and dot.
      return file.substr(0, extension_offset - 1);
    }
    case WHAT_EXTENSION: {
      return std::string(FindExtension(&input_string));
    }
    case WHAT_DIR: {
      std::string_view dir_incl_slash = FindDir(&input_string);
      if (dir_incl_slash.empty())
        return std::string(".");
      // Trim slash since this function doesn't return trailing slashes. The
      // times we don't do this are if the result is "/" and "//" since those
      // slashes can't be trimmed.
      if (dir_incl_slash == "/")
        return std::string("/.");
      if (dir_incl_slash == "//")
        return std::string("//.");
      return std::string(dir_incl_slash.substr(0, dir_incl_slash.size() - 1));
    }
    case WHAT_GEN_DIR: {
      return DirectoryWithNoLastSlash(GetSourceDir(
          BuildDirContext(settings),
          DirForInput(settings, current_dir, input, err), BuildDirType::GEN));
    }
    case WHAT_OUT_DIR: {
      return DirectoryWithNoLastSlash(GetSourceDir(
          BuildDirContext(settings),
          DirForInput(settings, current_dir, input, err), BuildDirType::OBJ));
    }
    case WHAT_ABSPATH: {
      bool as_dir =
          !input_string.empty() && input_string[input_string.size() - 1] == '/';

      return current_dir.ResolveRelativeAs(
          !as_dir, input, err, settings->build_settings()->root_path_utf8(),
          &input_string);
    }
    default:
      NOTREACHED();
      return std::string();
  }
}

}  // namespace

const char kGetPathInfo[] = "get_path_info";
const char kGetPathInfo_HelpShort[] =
    "get_path_info: Extract parts of a file or directory name.";
const char kGetPathInfo_Help[] =
    R"(get_path_info: Extract parts of a file or directory name.

  get_path_info(input, what)

  The first argument is either a string representing a file or directory name,
  or a list of such strings. If the input is a list the return value will be a
  list containing the result of applying the rule to each item in the input.

Possible values for the "what" parameter

  "file"
      The substring after the last slash in the path, including the name and
      extension. If the input ends in a slash, the empty string will be
      returned.
        "foo/bar.txt" => "bar.txt"
        "bar.txt" => "bar.txt"
        "foo/" => ""
        "" => ""

  "name"
     The substring of the file name not including the extension.
        "foo/bar.txt" => "bar"
        "foo/bar" => "bar"
        "foo/" => ""

  "extension"
      The substring following the last period following the last slash, or the
      empty string if not found. The period is not included.
        "foo/bar.txt" => "txt"
        "foo/bar" => ""

  "dir"
      The directory portion of the name, not including the slash.
        "foo/bar.txt" => "foo"
        "//foo/bar" => "//foo"
        "foo" => "."

      The result will never end in a slash, so if the resulting is empty, the
      system ("/") or source ("//") roots, a "." will be appended such that it
      is always legal to append a slash and a filename and get a valid path.

  "out_dir"
      The output file directory corresponding to the path of the given file,
      not including a trailing slash.
        "//foo/bar/baz.txt" => "//out/Default/obj/foo/bar"

  "gen_dir"
      The generated file directory corresponding to the path of the given file,
      not including a trailing slash.
        "//foo/bar/baz.txt" => "//out/Default/gen/foo/bar"

  "abspath"
      The full absolute path name to the file or directory. It will be resolved
      relative to the current directory, and then the source- absolute version
      will be returned. If the input is system- absolute, the same input will
      be returned.
        "foo/bar.txt" => "//mydir/foo/bar.txt"
        "foo/" => "//mydir/foo/"
        "//foo/bar" => "//foo/bar"  (already absolute)
        "/usr/include" => "/usr/include"  (already absolute)

      If you want to make the path relative to another directory, or to be
      system-absolute, see rebase_path().

Examples
  sources = [ "foo.cc", "foo.h" ]
  result = get_path_info(source, "abspath")
  # result will be [ "//mydir/foo.cc", "//mydir/foo.h" ]

  result = get_path_info("//foo/bar/baz.cc", "dir")
  # result will be "//foo/bar"

  # Extract the source-absolute directory name,
  result = get_path_info(get_path_info(path, "dir"), "abspath")
)";

Value RunGetPathInfo(Scope* scope,
                     const FunctionCallNode* function,
                     const std::vector<Value>& args,
                     Err* err) {
  if (args.size() != 2) {
    *err = Err(function, "Expecting two arguments to get_path_info.");
    return Value();
  }

  // Extract the "what".
  if (!args[1].VerifyTypeIs(Value::STRING, err))
    return Value();
  What what;
  if (args[1].string_value() == "file") {
    what = WHAT_FILE;
  } else if (args[1].string_value() == "name") {
    what = WHAT_NAME;
  } else if (args[1].string_value() == "extension") {
    what = WHAT_EXTENSION;
  } else if (args[1].string_value() == "dir") {
    what = WHAT_DIR;
  } else if (args[1].string_value() == "out_dir") {
    what = WHAT_OUT_DIR;
  } else if (args[1].string_value() == "gen_dir") {
    what = WHAT_GEN_DIR;
  } else if (args[1].string_value() == "abspath") {
    what = WHAT_ABSPATH;
  } else {
    *err = Err(args[1], "Unknown value for 'what'.");
    return Value();
  }

  const SourceDir& current_dir = scope->GetSourceDir();
  if (args[0].type() == Value::STRING) {
    return Value(function, GetOnePathInfo(scope->settings(), current_dir, what,
                                          args[0], err));
  } else if (args[0].type() == Value::LIST) {
    const std::vector<Value>& input_list = args[0].list_value();
    Value result(function, Value::LIST);
    for (const auto& cur : input_list) {
      result.list_value().push_back(Value(
          function,
          GetOnePathInfo(scope->settings(), current_dir, what, cur, err)));
      if (err->has_error())
        return Value();
    }
    return result;
  }

  *err = Err(args[0], "Path must be a string or a list of strings.");
  return Value();
}

}  // namespace functions
