// file      : libbuild2/script/script.hxx -*- C++ -*-
// license   : MIT; see accompanying LICENSE file

#ifndef LIBBUILD2_SCRIPT_SCRIPT_HXX
#define LIBBUILD2_SCRIPT_SCRIPT_HXX

#include <libbuild2/types.hxx>
#include <libbuild2/forward.hxx>
#include <libbuild2/utility.hxx>

#include <libbuild2/token.hxx>
#include <libbuild2/variable.hxx>

namespace build2
{
  namespace script
  {
    // Pre-parsed representation.
    //

    enum class line_type
    {
      var,
      cmd,
      cmd_if,
      cmd_ifn,
      cmd_elif,
      cmd_elifn,
      cmd_else,
      cmd_end
    };

    ostream&
    operator<< (ostream&, line_type);

    struct line
    {
      line_type type;
      replay_tokens tokens;

      union
      {
        const variable* var; // Pre-entered for line_type::var.
      };
    };

    // Most of the time we will have just one line (a command).
    //
    using lines = small_vector<line, 1>;

    // Print the script lines, trying to reproduce their original (non-
    // expanded) representation.
    //
    // Note that the exact spacing and partial quoting may not be restored due
    // to the information loss.
    //
    void
    dump (ostream&, const string& ind, const lines&);

    // As above but print a single line and without the trailing newline token
    // by default.
    //
    void
    dump (ostream&, const line&, bool newline = false);

    // Parse object model.
    //

    // redirect
    //
    enum class redirect_type
    {
      // No data is allowed to be read or written.
      //
      // Note that redirect of this type cannot be currently specified on the
      // script command line and can only be set via the environment object
      // as a default redirect (see below).
      //
      none,

      pass,
      null,
      trace,
      merge,
      here_str_literal,
      here_str_regex,
      here_doc_literal,
      here_doc_regex,
      here_doc_ref,     // Reference to here_doc literal or regex.
      file,
    };

    // Pre-parsed (but not instantiated) regex lines. The idea here is that
    // we should be able to re-create their (more or less) exact text
    // representation for diagnostics but also instantiate without any
    // re-parsing.
    //
    struct regex_line
    {
      // If regex is true, then value is the regex expression. Otherwise, it
      // is a literal. Note that special characters can be present in both
      // cases. For example, //+ is a regex, while /+ is a literal, both
      // with '+' as a special character. Flags are only valid for regex.
      // Literals falls apart into textual (has no special characters) and
      // special (has just special characters instead) ones. For example
      // foo is a textual literal, while /.+ is a special one. Note that
      // literal must not have value and special both non-empty.
      //
      bool regex;

      string value;
      string flags;
      string special;

      uint64_t line;
      uint64_t column;

      // Create regex with optional special characters.
      //
      regex_line (uint64_t l, uint64_t c,
                  string v, string f, string s = string ())
          : regex (true),
            value (move (v)),
            flags (move (f)),
            special (move (s)),
            line (l),
            column (c) {}

      // Create a literal, either text or special.
      //
      regex_line (uint64_t l, uint64_t c, string v, bool s)
          : regex (false),
            value (s ? string () : move (v)),
            special (s ? move (v) : string ()),
            line (l),
            column (c) {}
    };

    struct regex_lines
    {
      char intro;   // Introducer character.
      string flags; // Global flags (here-document).

      small_vector<regex_line, 8> lines;
    };

    // Output file redirect mode.
    //
    enum class redirect_fmode
    {
      compare,
      overwrite,
      append
    };

    struct redirect
    {
      redirect_type type;

      struct file_type
      {
        using path_type = build2::path;
        path_type path;
        redirect_fmode mode; // Meaningless for input redirect.
      };

      union
      {
        int         fd;    // Merge-to descriptor.
        string      str;   // Note: with trailing newline, if requested.
        regex_lines regex; // Note: with trailing blank, if requested.
        file_type   file;
        reference_wrapper<const redirect> ref; // Note: no chains.
      };

      // Modifiers and the original representation (potentially an alias).
      //
      build2::token token;

      string end;         // Here-document end marker (no regex intro/flags).
      uint64_t end_line;  // Here-document end marker location.
      uint64_t end_column;

      // Create redirect of a type other than reference.
      //
      explicit
      redirect (redirect_type);

      // Create redirect of the reference type.
      //
      redirect (redirect_type t, const redirect& r, build2::token tk)
          : type (redirect_type::here_doc_ref),
            ref (r),
            token (move (tk))
      {
        // There is no support (and need) for reference chains.
        //
        assert (t == redirect_type::here_doc_ref &&
                r.type != redirect_type::here_doc_ref);
      }

      // Create redirect of the merge type.
      //
      // Note that it's the caller's responsibility to make sure that the file
      // descriptor is valid for this redirect (2 for stdout, etc).
      //
      redirect (redirect_type t, int f)
          : type (redirect_type::merge), fd (f)
      {
        assert (t == redirect_type::merge && (f == 1 || f == 2));
      }

      // Movable-only type.
      //
      redirect (redirect&&) noexcept;
      redirect& operator= (redirect&&) noexcept;

      redirect (const redirect&) = delete;
      redirect& operator= (const redirect&) = delete;

      ~redirect ();

      const redirect&
      effective () const noexcept
      {
        return type == redirect_type::here_doc_ref ? ref.get () : *this;
      }

      const string&
      modifiers () const noexcept
      {
        return token.value;
      }
    };

    // cleanup
    //
    enum class cleanup_type
    {
      always, // &foo  - cleanup, fail if does not exist.
      maybe,  // &?foo - cleanup, ignore if does not exist.
      never   // &!foo - don’t cleanup, ignore if doesn’t exist.
    };

    // File or directory to be automatically cleaned up at the end of the
    // script execution. If the path ends with a trailing slash, then it is
    // assumed to be a directory, otherwise -- a file. A directory that is
    // about to be cleaned up must be empty.
    //
    // The last component in the path may contain a wildcard that have the
    // following semantics:
    //
    // dir/*   - remove all immediate files
    // dir/*/  - remove all immediate sub-directories (must be empty)
    // dir/**  - remove all files recursively
    // dir/**/ - remove all sub-directories recursively (must be empty)
    // dir/*** - remove directory dir with all files and sub-directories
    //           recursively
    //
    struct cleanup
    {
      cleanup_type type;
      build2::path path;
    };
    using cleanups = vector<cleanup>;

    // command_exit
    //
    enum class exit_comparison {eq, ne};

    struct command_exit
    {
      // C/C++ don't apply constraints on program exit code other than it
      // being of type int.
      //
      // POSIX specifies that only the least significant 8 bits shall be
      // available from wait() and waitpid(); the full value shall be
      // available from waitid() (read more at _Exit, _exit Open Group
      // spec).
      //
      // While the Linux man page for waitid() doesn't mention any
      // deviations from the standard, the FreeBSD implementation (as of
      // version 11.0) only returns 8 bits like the other wait*() calls.
      //
      // Windows supports 32-bit exit codes.
      //
      // Note that in shells some exit values can have special meaning so
      // using them can be a source of confusion. For bash values in the
      // [126, 255] range are such a special ones (see Appendix E, "Exit
      // Codes With Special Meanings" in the Advanced Bash-Scripting Guide).
      //
      exit_comparison comparison;
      uint8_t code;
    };

    // command
    //
    // Align with butl::process_env, assuming it is not very common to (un)set
    // more than two variables.
    //
    using environment_vars = small_vector<string, 2>;

    struct command
    {
      // We use NULL initial as an indication that the path stored in recall
      // is a program name that still needs to be resolved into the builtin
      // function or the process path.
      //
      process_path program;

      strings          arguments;
      environment_vars variables;

      optional<redirect> in;
      optional<redirect> out;
      optional<redirect> err;

      script::cleanups cleanups;

      // If nullopt, then the command is expected to succeed (0 exit code).
      //
      optional<command_exit> exit;
    };

    enum class command_to_stream: uint16_t
    {
      header   = 0x01,
      here_doc = 0x02,              // Note: printed on a new line.
      all      = header | here_doc
    };

    void
    to_stream (ostream&, const command&, command_to_stream);

    ostream&
    operator<< (ostream&, const command&);

    // command_pipe
    //
    using command_pipe = vector<command>;

    void
    to_stream (ostream&, const command_pipe&, command_to_stream);

    ostream&
    operator<< (ostream&, const command_pipe&);

    // command_expr
    //
    enum class expr_operator {log_or, log_and};

    struct expr_term
    {
      expr_operator op;  // OR-ed to an implied false for the first term.
      command_pipe pipe;
    };

    using command_expr = vector<expr_term>;

    void
    to_stream (ostream&, const command_expr&, command_to_stream);

    ostream&
    operator<< (ostream&, const command_expr&);

    // Script execution environment.
    //
    class environment
    {
    public:
      build2::context& context;

      // The platform script programs run on.
      //
      const target_triplet& host;

      // The work directory is used as the builtin/process CWD and to complete
      // relative paths. Any attempt to remove or move this directory (or its
      // parent directory) using the rm or mv builtins will fail. Must be an
      // absolute path.
      //
      const dir_name_view work_dir;

      // If the sanbox directory is not NULL, then any attempt to remove or
      // move a filesystem entry outside this directory using an explicit
      // cleanup or the rm/mv builtins will fail, unless the --force option is
      // specified for the builtin. Must be an absolute path.
      //
      const dir_name_view sandbox_dir;

      // The temporary directory is used by the script running machinery to
      // create special files. Must be an absolute path, unless empty. Can be
      // empty until the create_temp_dir() function call, which can be used
      // for creating this directory on demand.
      //
      const dir_path& temp_dir;

      // If true, the temporary directory will not be removed on the script
      // failure. In particular, this allows the script running machinery to
      // refer to the special files in diagnostics.
      //
      const bool temp_dir_keep;

      // Default process streams redirects.
      //
      // If a stream redirect is not specified on the script command line,
      // then the respective redirect data member will be used as the default.
      //
      const redirect in;
      const redirect out;
      const redirect err;

      environment (build2::context& ctx,
                   const target_triplet& h,
                   const dir_name_view& wd,
                   const dir_name_view& sd,
                   const dir_path& td, bool tk,
                   redirect&& i = redirect (redirect_type::pass),
                   redirect&& o = redirect (redirect_type::pass),
                   redirect&& e = redirect (redirect_type::pass))
          : context (ctx), host (h),
            work_dir (wd), sandbox_dir (sd), temp_dir (td), temp_dir_keep (tk),
            in (move (i)), out (move (o)), err (move (e))
      {
      }

      // Create environment without the sandbox.
      //
      environment (build2::context& ctx,
                   const target_triplet& h,
                   const dir_name_view& wd,
                   const dir_path& td, bool tk,
                   redirect&& i = redirect (redirect_type::pass),
                   redirect&& o = redirect (redirect_type::pass),
                   redirect&& e = redirect (redirect_type::pass))
          : environment (ctx, h,
                         wd, dir_name_view (), td, tk,
                         move (i), move (o), move (e))
      {
      }

      // Cleanup.
      //
    public:
      script::cleanups cleanups;
      paths special_cleanups;

      // Register a cleanup. If the cleanup is explicit, then override the
      // cleanup type if this path is already registered. Ignore implicit
      // registration of a path outside root directory (see below).
      //
      void
      clean (cleanup, bool implicit);

      // Register cleanup of a special file. Such files are created to
      // maintain the script running machinery and must be removed first, not
      // to interfere with the user-defined wildcard cleanups if the working
      // and temporary directories are the same.
      //
      void
      clean_special (path);

    public:
      // Set variable value with optional (non-empty) attributes.
      //
      virtual void
      set_variable (string&& name,
                    names&&,
                    const string& attrs,
                    const location&) = 0;

      // Create the temporary directory and set the temp_dir reference target
      // to its path. Must only be called if temp_dir is empty.
      //
      virtual void
      create_temp_dir () = 0;

    public:
      virtual
      ~environment () = default;
    };
  }
}

#include <libbuild2/script/script.ixx>

#endif // LIBBUILD2_SCRIPT_SCRIPT_HXX