// file      : bbot/machine-manifest.cxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#include <bbot/machine-manifest.hxx>

#include <sstream>

#include <libbutl/tab-parser.hxx>
#include <libbutl/string-parser.hxx>
#include <libbutl/manifest-parser.hxx>
#include <libbutl/manifest-serializer.hxx>

using namespace std;
using namespace butl;

namespace bbot
{
  using parser = manifest_parser;
  using parsing = manifest_parsing;
  using serializer = manifest_serializer;
  using serialization = manifest_serialization;
  using name_value = manifest_name_value;

  // machine_type
  //
  string
  to_string (machine_type t)
  {
    switch (t)
    {
    case machine_type::kvm:    return "kvm";
    case machine_type::nspawn: return "nspawn";
    }

    assert (false);
    return string ();
  }

  machine_type
  to_machine_type (const string& t)
  {
         if (t == "kvm")    return machine_type::kvm;
    else if (t == "nspawn") return machine_type::nspawn;
    else throw invalid_argument ("invalid machine type '" + t + '\'');
  }

  // machine_manifest
  //
  machine_manifest::
  machine_manifest (parser& p, bool iu)
      : machine_manifest (p, p.next (), iu)
  {
    // Make sure this is the end.
    //
    name_value nv (p.next ());
    if (!nv.empty ())
      throw parsing (p.name (), nv.name_line, nv.name_column,
                     "single machine manifest expected");
  }

  machine_manifest::
  machine_manifest (parser& p, name_value nv, bool iu)
      : machine_header_manifest (p,
                                 move (nv),
                                 manifest_unknown_mode::stop,
                                 &nv)
  {
    auto bad_name = [&p, &nv] (const string& d)
    {
      throw parsing (p.name (), nv.name_line, nv.name_column, d);
    };

    // Offsets are used to tie an error to the specific position inside a
    // manifest value (possibly a multiline one).
    //
    auto bad_value = [&p, &nv] (
      const string& d, uint64_t column_offset = 0, uint64_t line_offset = 0)
    {
      throw parsing (p.name (),
                     nv.value_line + line_offset,
                     (line_offset == 0 ? nv.value_column : 1) + column_offset,
                     d);
    };

    optional<machine_type> type;

    for (; !nv.empty (); nv = p.next ())
    {
      string& n (nv.name);
      string& v (nv.value);

      if (n == "type")
      {
        if (type)
          bad_name ("machine type redefinition");

        try
        {
          type = to_machine_type (v);
        }
        catch (const invalid_argument&)
        {
          bad_value ("invalid machine type");
        }
      }
      else if (n == "mac")
      {
        if (mac)
          bad_name ("machine mac redefinition");

        // @@ Should we check that the value is a valid mac?
        //
        mac = move (v);
      }
      else if (n == "options")
      {
        if (options)
          bad_name ("machine options redefinition");

        strings op;

        // Note that when reporting errors we combine the manifest value
        // position with the respective error position.
        //
        try
        {
          istringstream is (v);
          tab_parser parser (is, "");

          tab_fields tl;
          while (!(tl = parser.next ()).empty ())
          {
            for (auto& tf: tl)
              op.emplace_back (move (tf.value));
          }
        }
        catch (const tab_parsing& e)
        {
          bad_value ("invalid machine options: " + e.description,
                     e.column - 1,
                     e.line - 1);
        }

        if (op.empty ())
          bad_value ("empty machine options");

        options = move (op);
      }
      else if (n == "changes")
      {
        if (v.empty ())
          bad_value ("empty machine changes");

        changes.emplace_back (move (v));
      }
      else if (!iu)
        bad_name ("unknown name '" + n + "' in machine manifest");
    }

    // Verify all non-optional values were specified.
    //
    if (!type)
      bad_value ("no machine type specified");

    this->type = *type;
  }

  void machine_manifest::
  serialize (serializer& s) const
  {
    // @@ Should we check that all non-optional values are specified and all
    //    values are valid?
    //

    machine_header_manifest::serialize (s, false);

    s.next ("type", to_string (type));

    if (mac)
      s.next ("mac", *mac);

    // Recompose options string as a space-separated option list,
    //
    if (options)
    {
      string v;
      for (auto b (options->cbegin ()), i (b), e (options->cend ()); i != e;
           ++i)
      {
        if (i != b)
          v += ' ';

        v += *i;
      }

      s.next ("options", v);
    }

    for (const string& c: changes)
      s.next ("changes", c);

    s.next ("", ""); // End of manifest.
  }

  strings machine_manifest::
  unquoted_options () const
  {
    return options
      ? string_parser::unquote (*options)
      : strings ();
  }

  // toolchain_manifest
  //
  toolchain_manifest::
  toolchain_manifest (parser& p, bool iu)
      : toolchain_manifest (p, p.next (), iu)
  {
    // Make sure this is the end.
    //
    name_value nv (p.next ());
    if (!nv.empty ())
      throw parsing (p.name (), nv.name_line, nv.name_column,
                     "single toolchain manifest expected");
  }

  toolchain_manifest::
  toolchain_manifest (parser& p, name_value nv, bool iu)
  {
    auto bad_name = [&p, &nv] (const string& d)
    {
      throw parsing (p.name (), nv.name_line, nv.name_column, d);
    };

    auto bad_value = [&p, &nv] (const string& d)
    {
      throw parsing (p.name (), nv.value_line, nv.value_column, d);
    };

    // Make sure this is the start and we support the version.
    //
    if (!nv.name.empty ())
      bad_name ("start of toolchain manifest expected");

    if (nv.value != "1")
      bad_value ("unsupported format version");

    // Parse the toolchain manifest.
    //
    for (nv = p.next (); !nv.empty (); nv = p.next ())
    {
      string& n (nv.name);
      string& v (nv.value);

      if (n == "id")
      {
        if (!id.empty ())
          bad_name ("toolchain id redefinition");

        if (v.empty ())
          bad_value ("empty toolchain id");

        id = move (v);
      }
      else if (!iu)
        bad_name ("unknown name '" + n + "' in toolchain manifest");
    }

    // Verify all non-optional values were specified.
    //
    if (id.empty ())
      bad_value ("no toolchain id specified");
  }

  void toolchain_manifest::
  serialize (serializer& s) const
  {
    // @@ Should we check that all non-optional values are specified?
    //
    s.next ("", "1"); // Start of manifest.
    s.next ("id", id);
    s.next ("", ""); // End of manifest.
  }

  // bootstrapped_machine_manifest
  //
  bootstrapped_machine_manifest::
  bootstrapped_machine_manifest (parser& p, bool iu)
  {
    name_value nv (p.next ());

    auto bad_name = [&p, &nv] (const string& d)
    {
      throw parsing (p.name (), nv.name_line, nv.name_column, d);
    };

    auto bad_value = [&p, &nv] (const string& d)
    {
      throw parsing (p.name (), nv.value_line, nv.value_column, d);
    };

    // Make sure this is the start and we support the version.
    //
    if (!nv.name.empty ())
      bad_name ("start of bootstrapped machine manifest expected");

    if (nv.value != "1")
      bad_value ("unsupported format version");

    // Parse the bootstrapped machine manifest. Currently there is no values
    // expected.
    //
    for (nv = p.next (); !nv.empty (); nv = p.next ())
    {
      if (!iu)
        bad_name ("unknown name '" + nv.name +
                  "' in bootstrapped machine manifest");
    }

    nv = p.next ();
    if (nv.empty ())
      bad_value ("machine manifest expected");

    machine = machine_manifest (p, nv, iu);

    if (!machine.mac)
      bad_name ("mac address must be present in machine manifest");

    if (machine.effective_role () == machine_role::build)
    {
      nv = p.next ();
      if (nv.empty ())
        bad_value ("toolchain manifest expected");

      toolchain = toolchain_manifest (p, nv, iu);

      nv = p.next ();
      if (nv.empty ())
        bad_value ("bootstrap manifest expected");

      bootstrap = bootstrap_manifest (p, nv, iu);

      // Make sure this is the end.
      //
      nv = p.next ();
      if (!nv.empty ())
        throw parsing (p.name (), nv.name_line, nv.name_column,
                       "single bootstrapped machine manifest expected");
    }
    else
    {
      // Make sure this is the end.
      //
      nv = p.next ();
      if (!nv.empty ())
        throw parsing (p.name (), nv.name_line, nv.name_column,
                       "single machine manifest expected");
    }
  }

  void bootstrapped_machine_manifest::
  serialize (serializer& s) const
  {
    // @@ Should we check that all non-optional values are specified?
    //
    s.next ("", "1"); // Start of manifest.
    s.next ("", "");  // End of manifest.

    if (!machine.mac)
      throw serialization (s.name (),
                           "mac address must be present in machine manifest");

    machine.serialize (s);

    if (machine.effective_role () == machine_role::build)
    {
      toolchain.serialize (s);
      bootstrap.serialize (s);
    }

    s.next ("", ""); // End of stream.
  }
}
