//
// Copyright (C) 2023 The Android Open Source Project
//
// 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
//
//      http://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.

#include "cuttlefish/host/libs/web/android_build_string.h"

#include <stddef.h>

#include <optional>
#include <ostream>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include <fmt/ostream.h>
#include <fmt/ranges.h>

#include "cuttlefish/flag_parser/flag.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {

namespace {

Result<std::pair<std::string, std::optional<std::string>>> ParseFilepath(
    std::string_view build_string) {
  std::string_view remaining_build_string = build_string;
  std::optional<std::string> filepath;
  size_t open_bracket = build_string.find('{');
  size_t close_bracket = build_string.find('}');

  bool has_open = open_bracket != std::string::npos;
  bool has_close = close_bracket != std::string::npos;
  CF_EXPECTF(
      has_open == has_close,
      "Open or close curly bracket exists without its complement in \"{}\"",
      build_string);
  if (has_open && has_close) {
    std::string_view remaining_substring = build_string.substr(0, open_bracket);
    CF_EXPECTF(
        !remaining_substring.empty(),
        "The build string excluding filepath cannot be empty.  Input: {}",
        build_string);
    size_t filepath_start = open_bracket + 1;
    std::string_view filepath_substring =
        build_string.substr(filepath_start, close_bracket - filepath_start);
    CF_EXPECTF(
        !filepath_substring.empty(),
        "The filepath between positions {},{} cannot be empty.  Input: {}",
        filepath_start, close_bracket, build_string);
    remaining_build_string = remaining_substring;
    filepath = filepath_substring;
  }
  return {{std::string(remaining_build_string), filepath}};
}

Result<BuildString> ParseDeviceBuildString(
    const std::string& build_string,
    const std::optional<std::string>& filepath) {
  size_t slash_pos = build_string.find('/');
  std::string branch_or_id = build_string.substr(0, slash_pos);
  auto result = DeviceBuildString{
      .branch_or_id = build_string.substr(0, slash_pos), .filepath = filepath};
  if (slash_pos != std::string::npos) {
    size_t next_slash_pos = build_string.find('/', slash_pos + 1);
    CF_EXPECTF(next_slash_pos == std::string::npos,
               "Build string argument cannot have more than one '/'.  Found at "
               "positions {},{}.",
               slash_pos, next_slash_pos);
    result.target = build_string.substr(slash_pos + 1);
  }
  return result;
}

Result<DirectoryBuildString> ParseDirectoryBuildString(
    const std::string& build_string,
    const std::optional<std::string>& filepath) {
  auto result = DirectoryBuildString{.filepath = filepath};
  std::vector<std::string> split = absl::StrSplit(build_string, ':');
  result.target = split.back();
  split.pop_back();
  result.paths = std::move(split);
  return result;
}

}  // namespace

std::ostream& operator<<(std::ostream& out,
                         const DeviceBuildString& build_string) {
  fmt::print(out, "(branch_or_id=\"{}\", target=\"{}\", filepath=\"{}\")",
             build_string.branch_or_id, build_string.target.value_or(""),
             build_string.filepath.value_or(""));
  return out;
}

std::ostream& operator<<(std::ostream& out,
                         const DirectoryBuildString& build_string) {
  fmt::print(out, "(paths=\"{}\", target=\"{}\", filepath=\"{}\")",
             fmt::join(build_string.paths, ":"), build_string.target,
             build_string.filepath.value_or(""));
  return out;
}

std::ostream& operator<<(std::ostream& out, const BuildString& build_string) {
  std::visit([&out](auto&& arg) { out << arg; }, build_string);
  return out;
}

std::ostream& operator<<(std::ostream& out,
                         const std::optional<BuildString>& build_string) {
  if (build_string) {
    out << "has_value(" << *build_string << ")";
  } else {
    out << "no_value()";
  }
  return out;
}

std::optional<std::string> GetFilepath(const BuildString& build_string) {
  return std::visit([](auto&& arg) { return arg.filepath; }, build_string);
}

void SetFilepath(BuildString& build_string, const std::string& value) {
  std::visit([&value](auto&& arg) { arg.filepath = value; }, build_string);
}

Result<BuildString> ParseBuildString(std::string_view build_string) {
  CF_EXPECT(!build_string.empty(), "The given build string cannot be empty");
  auto [remaining_build_string, filepath] =
      CF_EXPECT(ParseFilepath(build_string));
  if (remaining_build_string.find(':') != std::string::npos) {
    return CF_EXPECT(
        ParseDirectoryBuildString(remaining_build_string, filepath));
  } else {
    return CF_EXPECT(ParseDeviceBuildString(remaining_build_string, filepath));
  }
}

Flag GflagsCompatFlag(const std::string& name,
                      std::optional<BuildString>& value) {
  return Flag::StringFlag(name)
      .Getter([&value]() {
        std::stringstream result;
        result << value;
        return result.str();
      })
      .Setter([&value](std::string_view arg) -> Result<void> {
        value = std::nullopt;
        if (!arg.empty()) {
          value = CF_EXPECT(ParseBuildString(arg));
        }
        return {};
      });
}

Flag GflagsCompatFlag(const std::string& name,
                      std::vector<std::optional<BuildString>>& value) {
  return Flag::StringFlag(name)
      .Getter([&value]() {
        return absl::StrJoin(value, ",", absl::StreamFormatter());
      })
      .Setter([&value](std::string_view arg) -> Result<void> {
        if (arg.empty()) {
          value.clear();
          return {};
        }
        std::vector<std::string> str_vals = absl::StrSplit(arg, ',');
        value.clear();
        for (const auto& str_val : str_vals) {
          if (str_val.empty()) {
            value.emplace_back(std::nullopt);
          } else {
            value.emplace_back(CF_EXPECT(ParseBuildString(str_val)));
          }
        }
        return {};
      });
}

namespace {
struct WithFallbackTargetVisitor {
  BuildString operator()(DeviceBuildString build_string,
                         const std::string& fallback) {
    if (!build_string.target) {
      build_string.target = std::move(fallback);
    }
    return build_string;
  }

  BuildString operator()(DirectoryBuildString build_string,
                         const std::string&) {
    return build_string;
  }
};
}  // namespace

BuildString WithFallbackTarget(BuildString build_string, std::string fallback) {
  std::variant<std::string> fallback_var(std::move(fallback));
  return std::visit(WithFallbackTargetVisitor(), build_string, fallback_var);
}

}  // namespace cuttlefish
