/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#ifndef ecflow_node_EcfFile_HPP
#define ecflow_node_EcfFile_HPP

#include <fstream>
#include <memory>

#include "ecflow/core/Filesystem.hpp"
#include "ecflow/node/NodeFwd.hpp"

// This class is used to minimise file I/0.
// When job processing the same include file can be opened and closed many time
// This cache serves to open the include file only once. It halves the job processing time.
class IncludeFileCache {
public:
    explicit IncludeFileCache(const std::string& path);
    // Disable copy (and move) semantics
    IncludeFileCache(const IncludeFileCache&)                  = delete;
    const IncludeFileCache& operator=(const IncludeFileCache&) = delete;

    ~IncludeFileCache();

    const std::string& path() const { return path_; }
    bool lines(std::vector<std::string>&);

private:
    std::string path_;
    std::ifstream fp_; // is not copy constructable
    size_t no_of_lines_;
};

/// This class is used in the pre-processing of files( .ecf or .usr or .man typically)
/// It is used to to create the job file.
///
///
/// Please note the %manual is only created on request from the file cmd.
/// Even then it is extracted as a string. i.e. no .man file is
/// created. This is left to the client.
/// When returning the manual we pre-process the files first
///
/// However for testing purpose this capability may be retained.

class EcfFile {
public:
    enum Origin {
        ECF_SCRIPT,    // Look for .ecf file, uses default algorithm to find %includes
        ECF_HOME,      // default directory for scripts
        ECF_FILES,     // User specified directory
        ECF_FETCH_CMD, // pre-process output of ECF_FETCH,     all %includes use same command
        ECF_SCRIPT_CMD // pre-process output of ECF_SCRIPT_CMD, uses default algorithm to find %includes
    };

    // Use to record how the ecf_file was located from ECF_FILES and ECF_HOME directories. Default is PRUNE_ROOT
    enum EcfFileSearchAlgorithm { PRUNE_ROOT, PRUNE_LEAF };

    EcfFile();

    /// This preserves the caches, used to avoid opening/stat of include file more than once.
    EcfFile& operator=(const EcfFile& rhs);

    /// use default copy constructor, assignment, destructor
    /// ECF_FETCH  is used obtain the script from running a command  i.e.
    /// from the version control system.
    EcfFile(Node*,
            const std::string& path_to_script_or_fetch_cmd,
            EcfFile::Origin                 = ECF_SCRIPT,
            EcfFile::EcfFileSearchAlgorithm = PRUNE_ROOT);

    // The path to the ecf file, empty path means that ecf file could not be located
    bool valid() const { return !script_path_or_cmd_.empty(); }

    /// This function will return the contents of %manual -> %end for the input file
    /// It will pre-process the file, then extract the manual form all the pre-processed files
    /// Will throw std::runtime_error for errors
    void manual(std::string& theManual);

    /// returns the script, The first line contains the origin of the file/cmd.
    /// Will throw std::runtime_error for errors
    void script(std::string& theScript) const;

    const std::string& script_path_or_cmd() const { return script_path_or_cmd_; }

    /// What is the origin of this ecf_file
    std::string ecf_file_origin_dump() const;

    /// simple accessors
    Origin ecf_file_origin() const { return script_origin_; }
    EcfFileSearchAlgorithm ecf_file_search_algorithm() const { return ecf_file_search_algorithm_; }
    static std::string origin_str(Origin);
    static std::string search_algorithm_str(EcfFileSearchAlgorithm);

    /// Create the job file from with script of a task or alias.
    /// Note: the location of the ecf file may not be the same as the job file
    ///       For creating the job we must use ECF_JOB
    ///
    /// _IF_ ECF_CLIENT is specified provide Special support for migration. TEST ONLY
    /// The variable ECF_CLIENT is used to specify the path to client exe.
    /// This is then used to replace smsinit,smscomplete, smsevent,smsmeter.smslabel,smsabort
    /// This function will start the pre processing
    /// Will throw std::runtime_error for errors
    const std::string& create_job(JobsParam&);

    /// Process the script file, to add all the used variables, add the start of the file
    /// between %comment %end, The augmented script is returned in file_with_used_variables
    /// Will throw std::runtime_error for errors
    void edit_used_variables(std::string& file_with_used_variables);

    /// Pre-processing involves include expansion and variable substitution
    /// Will throw std::runtime_error if pre processing fails
    void pre_process(std::string& pre_processed_file);

    /// Does full pre-processing:
    ///   - include pre-processing
    ///   - variable substitution
    ///   - remove comment and manual sections
    ///   - ignore %nopp
    void pre_process_user_file(std::vector<std::string>& user_edit_file, std::string& pre_processed_file);

    /// Searches for the first %comment, then extracts all variables of the form
    ///   <name>  =  <value>
    /// and places into map
    static void extract_used_variables(NameValueMap& used_variables_as_map,
                                       const std::vector<std::string>& script_lines);

    static std::vector<std::string> get_ecf_include_paths(const EcfFile& ecf);

private:
    friend class PreProcessor;
    enum Type { SCRIPT, INCLUDE, MANUAL, COMMENT };
    static std::string fileType(EcfFile::Type);

    bool open_script_file(const std::string& file,
                          EcfFile::Type,
                          std::vector<std::string>& lines,
                          std::string& errormsg) const;
    bool open_include_file(const std::string& file, std::vector<std::string>& lines, std::string& errormsg) const;

    bool replaceSmsChildCmdsWithEcf(const std::string& clientPath, std::string& errormsg);
    void variableSubstitution(const JobsParam&);
    const std::string& doCreateJobFile(JobsParam&) const;
    bool doCreateManFile(std::string& errormsg);
    bool extractManual(const std::vector<std::string>& lines,
                       std::vector<std::string>& theManualLines,
                       std::string& errormsg) const;

    void remove_comment_manual_and_nopp_tokens();

    static int countEcfMicro(const std::string& line, const std::string& ecfMicro);
    static void dump_expanded_script_file(const std::vector<std::string>& lines); // for DEBUG

    /// returns the extension, i.e for task->.ecf for alias->.usr, will throw if node_ is not task or alias
    const std::string& get_extn() const;

    bool
    do_popen(const std::string& the_cmd, EcfFile::Type, std::vector<std::string>& lines, std::string& errormsg) const;

    fs::path file_creation_path() const;
    std::string script_or_job_path() const;

    bool extract_ecfmicro(const std::string& line, std::string& ecfmicro, std::string& error_msg) const;

    /// User edit functions:
    void get_used_variables(std::string& used_variables) const;
    bool get_used_variables(NameValueMap& used_variables, std::string& errorMsg) const;
    void doCreateUsrFile() const;
    bool file_exists(const std::string& file_path) const;

    Node* node_{nullptr};               // Task or Alias or Container when pre-processing the man files
    std::string ecfMicroCache_;         // cache value of ECF_MICRO
    std::string script_path_or_cmd_;    // path to .ecf, .usr file or command
    std::vector<std::string> jobLines_; // Lines that will form the job file.

    mutable std::vector<std::shared_ptr<IncludeFileCache>> include_file_cache_; // only open include file once
    mutable std::vector<std::pair<std::string, bool>> file_stat_cache_;         // Minimise calls to stat/kernel calls
    mutable std::string job_size_;                       // to be placed in log file during job submission
    EcfFile::Origin script_origin_{EcfFile::ECF_SCRIPT}; // get script from a file, or from running a command
    EcfFile::EcfFileSearchAlgorithm ecf_file_search_algorithm_{
        EcfFile::PRUNE_ROOT}; // only used for ECF_FILES and ECF_HOME
};

// This class is used in expanding(pre-processing) the includes.
// The pre-processing is done in a depth first fashion (ECFLOW-673)
class PreProcessor {
public:
    explicit PreProcessor(EcfFile*, const char* error_context);
    // Disable copy (and move) semantics
    PreProcessor(const PreProcessor&)                  = delete;
    const PreProcessor& operator=(const PreProcessor&) = delete;

    ~PreProcessor();

    void preProcess(std::vector<std::string>& script_lines);

private:
    std::string error_context() const;

    // include pre-processing on the included file.
    // Note: include directives _in_ manual/comment should he handled.
    //       only include directives in %nopp/%end are ignored
    void preProcess_line();
    void preProcess_includes();
    std::string getIncludedFilePath(const std::string& include, const std::string& line);

private:
    EcfFile* ecfile_;
    const char* error_context_;

    std::string pp_nopp_;
    std::string pp_comment_;
    std::string pp_manual_;
    std::string pp_end_;
    std::string ecf_micro_; // constant until ecfmicro changes, then reset

    std::vector<std::string>& jobLines_;

    std::vector<std::pair<std::string, int>>
        globalIncludedFileSet_; // test for recursive includes, <no _of times it was included>
    std::vector<std::string> include_once_set_;

    bool nopp_{false};
    bool comment_{false};
    bool manual_{false};
};

#endif /* ecflow_node_EcfFile_HPP */
