// file      : libbuild2/test/script/script.hxx -*- C++ -*-
// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#ifndef LIBBUILD2_TEST_SCRIPT_SCRIPT_HXX
#define LIBBUILD2_TEST_SCRIPT_SCRIPT_HXX

#include <set>

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

#include <libbuild2/variable.hxx>

#include <libbuild2/test/target.hxx>

#include <libbuild2/test/script/token.hxx> // replay_tokens

namespace build2
{
  namespace test
  {
    namespace script
    {
      class parser; // Required by VC for 'friend class parser' declaration.

      // Pre-parse 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 (test command).
      //
      using lines = small_vector<line, 1>;

      // Parse object model.
      //

      // redirect
      //
      enum class redirect_type
      {
        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.
        };

        string modifiers;   // Redirect modifiers.
        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 = redirect_type::none);

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

        // Move constuctible/assignable-only type.
        //
        redirect (redirect&&);
        redirect& operator= (redirect&&);

        ~redirect ();

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

      // 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
      // scope. 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
      //
      struct command
      {
        path program;
        strings arguments;

        redirect in;
        redirect out;
        redirect err;

        script::cleanups cleanups;

        command_exit exit {exit_comparison::eq, 0};
      };

      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&);

      // command_type
      //
      enum class command_type {test, setup, teardown};

      // description
      //
      struct description
      {
        string id;
        string summary;
        string details;

        bool
        empty () const
        {
          return id.empty () && summary.empty () && details.empty ();
        }
      };

      // scope
      //
      class script;

      enum class scope_state {unknown, passed, failed};

      class scope
      {
      public:
        scope* const parent; // NULL for the root (script) scope.
        script&      root;   // Self for the root (script) scope.

        // The chain of if-else scope alternatives. See also if_cond_ below.
        //
        unique_ptr<scope> if_chain;

        // Note that if we pass the variable name as a string, then it will
        // be looked up in the wrong pool.
        //
        variable_map vars;

        const path& id_path;     // Id path ($@, relative in POSIX form).
        const dir_path& wd_path; // Working dir ($~, absolute and normalized).

        optional<description> desc;

        scope_state state = scope_state::unknown;
        test::script::cleanups cleanups;
        paths special_cleanups;

        // Variables.
        //
      public:
        // Lookup the variable starting from this scope, continuing with outer
        // scopes, then the target being tested, then the testscript target,
        // and then outer buildfile scopes (including testscript-type/pattern
        // specific).
        //
        lookup
        find (const variable&) const;

        // As above but only look for buildfile variables. If target_only is
        // false then also look in scopes of the test target (this should only
        // be done if the variable's visibility is target).
        //
        lookup
        find_in_buildfile (const string&, bool target_only = true) const;

        // Return a value suitable for assignment. If the variable does not
        // exist in this scope's map, then a new one with the NULL value is
        // added and returned. Otherwise the existing value is returned.
        //
        value&
        assign (const variable& var) {return vars.assign (var);}

        // Return a value suitable for append/prepend. If the variable does
        // not exist in this scope's map, then outer scopes are searched for
        // the same variable. If found then a new variable with the found
        // value is added to this scope and returned. Otherwise this function
        // proceeds as assign() above.
        //
        value&
        append (const variable&);

        // Reset special $*, $N variables based on the test.* values.
        //
        void
        reset_special ();

        // Cleanup.
        //
      public:
        // 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 script working directory.
        //
        void
        clean (cleanup, bool implicit);

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

      public:
        virtual
        ~scope () = default;

      protected:
        scope (const string& id, scope* parent, script& root);

        // Pre-parse data.
        //
      public:
        virtual bool
        empty () const = 0;

      protected:
        friend class parser;

        location start_loc_;
        location end_loc_;

        optional<line> if_cond_;
      };

      // group
      //
      class group: public scope
      {
      public:
        vector<unique_ptr<scope>> scopes;

      public:
        group (const string& id, group& p): scope (id, &p, p.root) {}

      protected:
        group (const string& id, script& r): scope (id, nullptr, r) {}

        // Pre-parse data.
        //
      public:
        virtual bool
        empty () const override
        {
          return
            !if_cond_ && // The condition expression can have side-effects.
            setup_.empty () &&
            tdown_.empty () &&
            find_if (scopes.begin (), scopes.end (),
                     [] (const unique_ptr<scope>& s)
                     {
                       return !s->empty ();
                     }) == scopes.end ();
        }

      private:
        friend class parser;

        lines setup_;
        lines tdown_;
      };

      // test
      //
      class test: public scope
      {
      public:
        test (const string& id, group& p): scope (id, &p, p.root) {}

        // Pre-parse data.
        //
      public:
        virtual bool
        empty () const override
        {
          return tests_.empty ();
        }

      private:
        friend class parser;

        lines tests_;
      };

      // script
      //
      class script_base // Make sure certain things are initialized early.
      {
      protected:
        script_base (const target& test_target,
                     const testscript& script_target);

      public:
        const target&        test_target;   // Target we are testing.
        const build2::scope& target_scope;  // Base scope of test target.
        const testscript&    script_target; // Target of the testscript file.

      public:
        variable_pool var_pool;
        mutable shared_mutex var_pool_mutex;

        const variable& test_var;      // test
        const variable& options_var;   // test.options
        const variable& arguments_var; // test.arguments
        const variable& redirects_var; // test.redirects
        const variable& cleanups_var;  // test.cleanups

        const variable& wd_var;       // $~
        const variable& id_var;       // $@
        const variable& cmd_var;      // $*
        const variable* cmdN_var[10]; // $N
      };

      class script: public script_base, public group
      {
      public:
        script (const target& test_target,
                const testscript& script_target,
                const dir_path& root_wd);

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

        // Pre-parse data.
        //
      private:
        friend class parser;

        // Testscript file paths. Specifically, replay_token::file points to
        // these paths.
        //
        std::set<path> paths_;
      };
    }
  }
}

#include <libbuild2/test/script/script.ixx>

#endif // LIBBUILD2_TEST_SCRIPT_SCRIPT_HXX