#include "libstore/derivations.hh"

#include <absl/strings/match.h>
#include <absl/strings/str_split.h>
#include <absl/strings/string_view.h>
#include <glog/logging.h>

#include "libproto/worker.pb.h"
#include "libstore/fs-accessor.hh"
#include "libstore/globals.hh"
#include "libstore/store-api.hh"
#include "libstore/worker-protocol.hh"
#include "libutil/istringstream_nocopy.hh"
#include "libutil/util.hh"

namespace nix {

// TODO(#statusor): looks like easy absl::Status conversion
void DerivationOutput::parseHashInfo(bool& recursive, Hash& hash) const {
  recursive = false;
  std::string algo = hashAlgo;

  if (std::string(algo, 0, 2) == "r:") {
    recursive = true;
    algo = std::string(algo, 2);
  }

  HashType hashType = parseHashType(algo);
  if (hashType == htUnknown) {
    throw Error(format("unknown hash algorithm '%1%'") % algo);
  }

  auto hash_ = Hash::deserialize(this->hash, hashType);
  hash = Hash::unwrap_throw(hash_);
}

nix::proto::Derivation_DerivationOutput DerivationOutput::to_proto() const {
  nix::proto::Derivation_DerivationOutput result;
  result.mutable_path()->set_path(path);
  result.set_hash_algo(hashAlgo);
  result.set_hash(hash);
  return result;
}

BasicDerivation BasicDerivation::from_proto(
    const nix::proto::Derivation* proto_derivation, const nix::Store& store) {
  BasicDerivation result;
  result.platform = proto_derivation->platform();
  result.builder = proto_derivation->builder().path();
  store.assertStorePath(result.builder);

  for (auto [k, v] : proto_derivation->outputs()) {
    result.outputs.emplace(k, v);
  }

  result.inputSrcs.insert(proto_derivation->input_sources().paths().begin(),
                          proto_derivation->input_sources().paths().end());

  result.args.insert(result.args.end(), proto_derivation->args().begin(),
                     proto_derivation->args().end());

  for (auto [k, v] : proto_derivation->env()) {
    result.env.emplace(k, v);
  }

  return result;
}

nix::proto::Derivation BasicDerivation::to_proto() const {
  nix::proto::Derivation result;
  for (const auto& [key, output] : outputs) {
    result.mutable_outputs()->insert({key, output.to_proto()});
  }
  for (const auto& input_src : inputSrcs) {
    result.mutable_input_sources()->add_paths(input_src);
  }
  result.set_platform(platform);
  result.mutable_builder()->set_path(builder);
  for (const auto& arg : args) {
    result.add_args(arg);
  }

  for (const auto& [key, value] : env) {
    result.mutable_env()->insert({key, value});
  }

  return result;
}

Path BasicDerivation::findOutput(const std::string& id) const {
  auto i = outputs.find(id);
  if (i == outputs.end()) {
    throw Error(format("derivation has no output '%1%'") % id);
  }
  return i->second.path;
}

bool BasicDerivation::isBuiltin() const {
  return std::string(builder, 0, 8) == "builtin:";
}

Path writeDerivation(const ref<Store>& store, const Derivation& drv,
                     const std::string& name, RepairFlag repair) {
  PathSet references;
  references.insert(drv.inputSrcs.begin(), drv.inputSrcs.end());
  for (auto& i : drv.inputDrvs) {
    references.insert(i.first);
  }
  /* Note that the outputs of a derivation are *not* references
     (that can be missing (of course) and should not necessarily be
     held during a garbage collection). */
  std::string suffix = name + drvExtension;
  std::string contents = drv.unparse();
  return settings.readOnlyMode
             ? store->computeStorePathForText(suffix, contents, references)
             : store->addTextToStore(suffix, contents, references, repair);
}

/* Read string `s' from stream `str'. */
static void expect(std::istream& str, const std::string& s) {
  char s2[s.size()];
  str.read(s2, s.size());
  if (std::string(s2, s.size()) != s) {
    throw FormatError(format("expected string '%1%'") % s);
  }
}

/* Read a C-style string from stream `str'. */
static std::string parseString(std::istream& str) {
  std::string res;
  expect(str, "\"");
  int c;
  while ((c = str.get()) != '"' && c != EOF) {
    if (c == '\\') {
      c = str.get();
      if (c == 'n') {
        res += '\n';
      } else if (c == 'r') {
        res += '\r';
      } else if (c == 't') {
        res += '\t';
      } else if (c == EOF) {
        throw FormatError("unexpected EOF while parsing C-style escape");
      } else {
        res += static_cast<char>(c);
      }
    } else {
      res += static_cast<char>(c);
    }
  }
  return res;
}

static Path parsePath(std::istream& str) {
  std::string s = parseString(str);
  if (s.empty() || s[0] != '/') {
    throw FormatError(format("bad path '%1%' in derivation") % s);
  }
  return s;
}

static bool endOfList(std::istream& str) {
  if (str.peek() == ',') {
    str.get();
    return false;
  }
  if (str.peek() == ']') {
    str.get();
    return true;
  }
  return false;
}

static StringSet parseStrings(std::istream& str, bool arePaths) {
  StringSet res;
  while (!endOfList(str)) {
    res.insert(arePaths ? parsePath(str) : parseString(str));
  }
  return res;
}

Derivation parseDerivation(const std::string& s) {
  Derivation drv;
  istringstream_nocopy str(s);
  expect(str, "Derive([");

  /* Parse the list of outputs. */
  while (!endOfList(str)) {
    DerivationOutput out;
    expect(str, "(");
    std::string id = parseString(str);
    expect(str, ",");
    out.path = parsePath(str);
    expect(str, ",");
    out.hashAlgo = parseString(str);
    expect(str, ",");
    out.hash = parseString(str);
    expect(str, ")");
    drv.outputs[id] = out;
  }

  /* Parse the list of input derivations. */
  expect(str, ",[");
  while (!endOfList(str)) {
    expect(str, "(");
    Path drvPath = parsePath(str);
    expect(str, ",[");
    drv.inputDrvs[drvPath] = parseStrings(str, false);
    expect(str, ")");
  }

  expect(str, ",[");
  drv.inputSrcs = parseStrings(str, true);
  expect(str, ",");
  drv.platform = parseString(str);
  expect(str, ",");
  drv.builder = parseString(str);

  /* Parse the builder arguments. */
  expect(str, ",[");
  while (!endOfList(str)) {
    drv.args.push_back(parseString(str));
  }

  /* Parse the environment variables. */
  expect(str, ",[");
  while (!endOfList(str)) {
    expect(str, "(");
    std::string name = parseString(str);
    expect(str, ",");
    std::string value = parseString(str);
    expect(str, ")");
    drv.env[name] = value;
  }

  expect(str, ")");
  return drv;
}

Derivation readDerivation(const Path& drvPath) {
  try {
    return parseDerivation(readFile(drvPath));
  } catch (FormatError& e) {
    throw Error(format("error parsing derivation '%1%': %2%") % drvPath %
                e.msg());
  }
}

Derivation Store::derivationFromPath(const Path& drvPath) {
  assertStorePath(drvPath);
  ensurePath(drvPath);
  auto accessor = getFSAccessor();
  try {
    return parseDerivation(accessor->readFile(drvPath));
  } catch (FormatError& e) {
    throw Error(format("error parsing derivation '%1%': %2%") % drvPath %
                e.msg());
  }
}

const char* findChunk(const char* begin) {
  while (*begin != 0 && *begin != '\"' && *begin != '\\' && *begin != '\n' &&
         *begin != '\r' && *begin != '\t') {
    begin++;
  }

  return begin;
}

static void printString(std::string& res, const std::string& s) {
  res += '"';

  const char* it = s.c_str();
  while (*it != 0) {
    const char* end = findChunk(it);
    std::copy(it, end, std::back_inserter(res));

    it = end;

    switch (*it) {
      case '"':
      case '\\':
        res += "\\";
        res += *it;
        break;
      case '\n':
        res += "\\n";
        break;
      case '\r':
        res += "\\r";
        break;
      case '\t':
        res += "\\t";
        break;
      default:
        continue;
    }

    it++;
  }

  res += '"';
}

template <class ForwardIterator>
static void printStrings(std::string& res, ForwardIterator i,
                         ForwardIterator j) {
  res += '[';
  bool first = true;
  for (; i != j; ++i) {
    if (first) {
      first = false;
    } else {
      res += ',';
    }
    printString(res, *i);
  }
  res += ']';
}

std::string Derivation::unparse() const {
  std::string s;
  s.reserve(65536);
  s += "Derive([";

  bool first = true;
  for (auto& i : outputs) {
    if (first) {
      first = false;
    } else {
      s += ',';
    }
    s += '(';
    printString(s, i.first);
    s += ',';
    printString(s, i.second.path);
    s += ',';
    printString(s, i.second.hashAlgo);
    s += ',';
    printString(s, i.second.hash);
    s += ')';
  }

  s += "],[";
  first = true;
  for (auto& i : inputDrvs) {
    if (first) {
      first = false;
    } else {
      s += ',';
    }
    s += '(';
    printString(s, i.first);
    s += ',';
    printStrings(s, i.second.begin(), i.second.end());
    s += ')';
  }

  s += "],";
  printStrings(s, inputSrcs.begin(), inputSrcs.end());

  s += ',';
  printString(s, platform);
  s += ',';
  printString(s, builder);
  s += ',';
  printStrings(s, args.begin(), args.end());

  s += ",[";
  first = true;
  for (auto& i : env) {
    if (first) {
      first = false;
    } else {
      s += ',';
    }
    s += '(';
    printString(s, i.first);
    s += ',';
    printString(s, i.second);
    s += ')';
  }

  s += "])";

  return s;
}

bool isDerivation(const std::string& fileName) {
  return absl::EndsWith(fileName, drvExtension);
}

bool BasicDerivation::isFixedOutput() const {
  return outputs.size() == 1 && outputs.begin()->first == "out" &&
         !outputs.begin()->second.hash.empty();
}

DrvHashes drvHashes;

/* Returns the hash of a derivation modulo fixed-output
   subderivations.  A fixed-output derivation is a derivation with one
   output (`out') for which an expected hash and hash algorithm are
   specified (using the `outputHash' and `outputHashAlgo'
   attributes).  We don't want changes to such derivations to
   propagate upwards through the dependency graph, changing output
   paths everywhere.

   For instance, if we change the url in a call to the `fetchurl'
   function, we do not want to rebuild everything depending on it
   (after all, (the hash of) the file being downloaded is unchanged).
   So the *output paths* should not change.  On the other hand, the
   *derivation paths* should change to reflect the new dependency
   graph.

   That's what this function does: it returns a hash which is just the
   hash of the derivation ATerm, except that any input derivation
   paths have been replaced by the result of a recursive call to this
   function, and that for fixed-output derivations we return a hash of
   its output path. */
Hash hashDerivationModulo(Store& store, Derivation drv) {
  /* Return a fixed hash for fixed-output derivations. */
  if (drv.isFixedOutput()) {
    auto i = drv.outputs.begin();
    return hashString(htSHA256, "fixed:out:" + i->second.hashAlgo + ":" +
                                    i->second.hash + ":" + i->second.path);
  }

  /* For other derivations, replace the inputs paths with recursive
     calls to this function.*/
  DerivationInputs inputs2;
  for (auto& i : drv.inputDrvs) {
    Hash h = drvHashes[i.first];
    if (!h) {
      assert(store.isValidPath(i.first));
      Derivation drv2 = readDerivation(store.toRealPath(i.first));
      h = hashDerivationModulo(store, drv2);
      drvHashes[i.first] = h;
    }
    inputs2[h.to_string(Base16, false)] = i.second;
  }
  drv.inputDrvs = inputs2;

  return hashString(htSHA256, drv.unparse());
}

DrvPathWithOutputs parseDrvPathWithOutputs(absl::string_view path) {
  auto pos = path.find('!');
  if (pos == absl::string_view::npos) {
    return DrvPathWithOutputs(path, std::set<std::string>());
  }

  return DrvPathWithOutputs(
      path.substr(0, pos),
      absl::StrSplit(path.substr(pos + 1), absl::ByChar(','),
                     absl::SkipEmpty()));
}

Path makeDrvPathWithOutputs(const Path& drvPath,
                            const std::set<std::string>& outputs) {
  return outputs.empty() ? drvPath
                         : drvPath + "!" + concatStringsSep(",", outputs);
}

bool wantOutput(const std::string& output,
                const std::set<std::string>& wanted) {
  return wanted.empty() || wanted.find(output) != wanted.end();
}

PathSet BasicDerivation::outputPaths() const {
  PathSet paths;
  for (auto& i : outputs) {
    paths.insert(i.second.path);
  }
  return paths;
}

Source& readDerivation(Source& in, Store& store, BasicDerivation& drv) {
  drv.outputs.clear();
  auto nr = readNum<size_t>(in);
  for (size_t n = 0; n < nr; n++) {
    auto name = readString(in);
    DerivationOutput o;
    in >> o.path >> o.hashAlgo >> o.hash;
    store.assertStorePath(o.path);
    drv.outputs[name] = o;
  }

  drv.inputSrcs = readStorePaths<PathSet>(store, in);
  in >> drv.platform >> drv.builder;
  drv.args = readStrings<Strings>(in);

  nr = readNum<size_t>(in);
  for (size_t n = 0; n < nr; n++) {
    auto key = readString(in);
    auto value = readString(in);
    drv.env[key] = value;
  }

  return in;
}

Sink& operator<<(Sink& out, const BasicDerivation& drv) {
  out << drv.outputs.size();
  for (auto& i : drv.outputs) {
    out << i.first << i.second.path << i.second.hashAlgo << i.second.hash;
  }
  out << drv.inputSrcs << drv.platform << drv.builder << drv.args;
  out << drv.env.size();
  for (auto& i : drv.env) {
    out << i.first << i.second;
  }
  return out;
}

std::string hashPlaceholder(const std::string& outputName) {
  // FIXME: memoize?
  return "/" + hashString(htSHA256, "nix-output:" + outputName)
                   .to_string(Base32, false);
}

}  // namespace nix
