/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

/** @file error.h The declaration of classes for error registration.
 * @ingroup libcgencommon_files  */

#if !defined(ERROR_H)
#define ERROR_H

#include <string>
#include <string_view>
#include <vector>
#include <cassert>
#include <tuple>
#include <climits>
#include <algorithm>
#include <memory>
#include <compare>
#include <cstring>

#ifndef _ASSERT
#define  _ASSERT(A) assert(A)
#elif !defined(_DEBUG) && !defined(NDEBUG)
#define NDEBUG
#endif

//strerror() is not thread safe and results in all kinds of
//compiler warnings. There are safe variants, but...
#ifdef _WIN32
inline std::string my_strerror(int errno_) {
    //Well, windows has strerror_s(), which is C11 standard
    char buff[512] = {'\0'};
    strerror_s(buff, sizeof(buff), errno_);
    return buff;
}
#else
//POSIX and GCC each have strerror_r(), but what a mess: https://linux.die.net/man/3/strerror_r
//The precondition macros listed there do not work on MacOS, so we do this instead.
//Note: "Outside a template, a discarded statement is fully checked. if constexpr is not a
//substitute for the #if preprocessing directive" So both branches must be valid C++.
//Even so, on Mac integers may be smaller than pointers, so we use a template to avoid
//checking the other branch.
template <typename strerror_r_ret_type>
struct errno_printer {
    static std::string print(int errno_) {
        char buff[512] = {'\0'};
        constexpr bool strerror_r_rets_int = std::is_same_v<strerror_r_ret_type, int>;
        constexpr bool strerror_r_rets_charp = std::is_same_v<strerror_r_ret_type, char*>;
        if constexpr (strerror_r_rets_int) {
            if (strerror_r(errno_, buff, sizeof(buff)))
                snprintf(buff, sizeof(buff), "Could not get error string for errno=%d", errno_);
        } else if constexpr (strerror_r_rets_charp) {
            char* b = (char*)strerror_r(errno_, buff, sizeof(buff));
            if (b != buff) {
                strncpy(buff, b, sizeof(buff) - 1);
                buff[sizeof(buff) - 1] = 0;
            }
        } else
            static_assert(strerror_r_rets_charp || strerror_r_rets_int, "strerror_r() returns neither char* nor int.");
        return buff;
    }
};
inline std::string my_strerror(int errno_) {
    char buff[1];
    using strerror_r_ret_type = decltype(strerror_r(errno_, buff, sizeof(buff)));
    return errno_printer<strerror_r_ret_type>::print(errno_);
}
#endif

template<typename T, typename... U> requires (std::equality_comparable_with<T, U> && ...)
bool is_any_of(T t, U... u) { return ((t == u) || ...); }

class FileLineCol;

/** Contains a distance in the input file between two positions.
 * Mostly to compare.*/
class FileLineColDiff
{
protected:
    friend class FileLineCol;
    bool same_file;  ///<True if the two locations are in the same file and include level
    unsigned line;   ///<The difference in lines
    int col;         ///<The difference in characters within the line
public:
    FileLineColDiff() : same_file(false), line(UINT_MAX), col(INT_MAX) {}//initializes to maximum diff
    FileLineColDiff(const FileLineCol &t, const FileLineCol &o);
    bool IsZero() const { return same_file && line==0 && col==0; }
    bool operator == (const FileLineColDiff&a) const { return same_file==a.same_file && line==a.line && col==a.col; }
    bool operator < (const FileLineColDiff&a) const { return std::tie(same_file, line, col) < std::tie(a.same_file, a.line, a.col); }
    bool operator > (const FileLineColDiff&a) const { return std::tie(a.same_file, a.line, a.col) < std::tie(same_file, line, col); }
    bool operator <= (const FileLineColDiff &a) const { return !operator>(a); }
    bool operator >= (const FileLineColDiff &a) const { return !operator<(a); }
};

/** Enumerates why a certain text is included.*/
enum class EInclusionReason
{
    TOP_LEVEL,  ///<This location is not included from anywhere - this is the main, top level file.
    PROCEDURE,  ///<This location is inside a procedure, that is invoked from elsewhere
    COPY,       ///<This location is inside an element, a copy of which has been created.
    PARAMETER,  ///<This location is inside a parameter at the invocation of a procedure passed to the procedure
    INCLUDE,    ///<This location is in an included file
};

/** Contains a position in the input files.
 *
 * We may parse multiple files in succession, thus we number them, so we can
 * refer to a location in any of them.
 * In case we include a piece of input (invoke a procedure or include a file), we
 * use these locations in a shared stack.*/
class FileLineCol
{
    friend class FileLineColDiff;
    std::shared_ptr<const FileLineCol> next;    ///<A location that references us.
    constexpr bool FileLineColOnlySmaller(const FileLineCol&a) const noexcept { return std::tie(file, line, col, reason) < std::tie(a.file, a.line, a.col, a.reason); }
    constexpr bool FileLineColOnlyEqual(const FileLineCol&a) const noexcept { return file==a.file && line==a.line && col==a.col && reason==a.reason; }
    unsigned Depth() const noexcept { return next ? next->Depth()+1 : 0; }
    const FileLineCol *Down(unsigned l) const noexcept { return l ? next->Down(l-1) : this; }
public:
    int file = -1;   ///<The number of the file. Names are stored in MscError::Files. -1 if invalid location.
    size_t line = 0; ///<The (1-based) line of the location 
    size_t col = 0;  ///<The (1-based) character inside the line
    EInclusionReason reason = EInclusionReason::TOP_LEVEL; ///<Why are we included
    constexpr FileLineCol() noexcept = default;
    constexpr FileLineCol(int a, size_t b, size_t c=0, EInclusionReason r= EInclusionReason::TOP_LEVEL) noexcept
        : file(a), line(b), col(c), reason(r) {}
    FileLineCol(const FileLineCol& o) noexcept = default;
    FileLineCol(FileLineCol&& o) noexcept = default;
    FileLineCol(const FileLineCol& o, size_t b, size_t c) noexcept : FileLineCol(o) { line = b;  col = c; }
    const FileLineCol * GetNext() const noexcept { return next.get(); }
    const FileLineCol * FindFirstOf(EInclusionReason r) const noexcept { return reason==r ? this : next ? next->FindFirstOf(r) : nullptr; }
    FileLineCol TopOfStack() const noexcept { return {file, line, col, reason}; }
    void MakeInvalid() noexcept { file = -1; reason = EInclusionReason::TOP_LEVEL; next.reset(); }
    constexpr bool IsInvalid() const noexcept {return file<0;}
    constexpr bool IsValid() const noexcept { return file>=0; }
    void Push(int a, size_t b, size_t c, EInclusionReason r);
    void Push(const FileLineCol &o) {Push(o.file, o.line, o.col, o.reason);}
    void Push(const FileLineCol &o, EInclusionReason r) { Push(o.file, o.line, o.col, r); }
    FileLineCol CreatePushed(int a, size_t b, size_t c, EInclusionReason r) const
        { FileLineCol ret(*this); ret.Push(a, b, c, r); return ret; }
    FileLineCol CreatePushed(const FileLineCol &o) const
        { return CreatePushed(o.file, o.line, o.col, o.reason); }
    FileLineCol CreatePushed(const FileLineCol &o, EInclusionReason r) const
        { return CreatePushed(o.file, o.line, o.col, r); }
    bool Pop() noexcept;
    //partial ordering because we ignore 'reason'
    std::strong_ordering operator <=> (const FileLineCol& a) const noexcept {
        const FileLineCol* x= this, *y = &a;
        while (x && y)
            if (x->FileLineColOnlySmaller(*y)) return std::strong_ordering::less;
            else if (y->FileLineColOnlySmaller(*x)) return std::strong_ordering::greater;
            else x = x->next.get(), y = y->next.get();
        //the common part is equal. In this case the longer one is larger
        return !x && !y ? std::strong_ordering::equal : !x ? std::strong_ordering::greater : std::strong_ordering::less;
    }
    bool operator ==(const FileLineCol& a) const noexcept { return std::is_eq(operator<=>(a)); }
    FileLineCol& operator = (const FileLineCol& o) noexcept = default;
    FileLineCol& operator = (FileLineCol&& o) noexcept = default;
    FileLineCol NextChar() const noexcept { return AdvanceCol(1); }
    FileLineCol AdvanceCol(unsigned u) const noexcept { FileLineCol tmp(*this); tmp.col += u; return tmp; }
    FileLineCol AdvanceUTF8(std::string_view text) const noexcept;
    std::string Print() const;
    bool Read(std::string_view &s);
    FileLineColDiff operator - (const FileLineCol &o) const { return FileLineColDiff(*this, o); }
};

/** A structure to describe a range within a file.*/
struct FileLineColRange {
    FileLineCol start; ///< The first character of the range
    FileLineCol end;   ///< The last character of the range. Equals `start` for single character ranges.
    FileLineColRange TopOfStack() const noexcept { return {start.TopOfStack(), end.TopOfStack()}; }
    FileLineColRange& IncStartCol(size_t i = 1) &  noexcept { start.col += i; return *this; }
    FileLineColRange&&IncStartCol(size_t i = 1) && noexcept { start.col += i; return std::move(*this);}
    void MakeInvalid() noexcept {start.MakeInvalid(); end.MakeInvalid();}
    constexpr bool IsInvalid() const noexcept {return start.IsInvalid() || end.IsInvalid();}
    void Push(const FileLineCol &o, EInclusionReason r) { start.Push(o, r); end.Push(o, r); }
    void Push(const FileLineColRange &o, EInclusionReason r) { start.Push(o.start, r); end.Push(o.end, r); }
    FileLineColRange CreatePushed(const FileLineCol &o, EInclusionReason r) const
        { return {start.CreatePushed(o, r), end.CreatePushed(o, r)}; }
    FileLineColRange CreatePushed(const FileLineColRange &o, EInclusionReason r) const
        { return {start.CreatePushed(o.start, r), end.CreatePushed(o.end, r)}; }
    std::strong_ordering operator<=>(const FileLineColRange &o) const noexcept = default;
    FileLineColRange &operator +=(const FileLineColRange &o) & noexcept { start = std::min(start, o.start); end = std::min(end, o.end); return *this; }
    FileLineColRange&&operator +=(const FileLineColRange &o) &&noexcept { start = std::min(start, o.start); end = std::min(end, o.end); return std::move(*this); }
    friend FileLineColRange operator +(const FileLineColRange& a, const FileLineColRange& b) noexcept
        { return FileLineColRange{std::min(a.start, b.start),std::max(a.end, b.end)}; }
    friend FileLineColRange operator +(FileLineColRange&& a, FileLineColRange&& b) noexcept
        { return FileLineColRange{std::move(std::min(a.start, b.start)), std::move(std::max(a.end, b.end))}; }
    std::string Print() const { return start.Print() + end.Print(); }
    /** Read a location range from the string. On success, we step 's' to the next char after & return true, else we leave us unchanged.*/
    bool Read(std::string_view &s) { FileLineColRange r;  if (r.start.Read(s) && r.end.Read(s)) { *this = r; return true; } return false; }
    FileLineCol After() const noexcept { return end.NextChar(); }
};

/** Returns which of two ranges are shorter.*/
struct file_line_range_length_compare
{
    bool operator() (const FileLineColRange &a, const FileLineColRange &b) const noexcept {
        if (b.end.file - b.start.file == a.end.file - a.start.file) {
            if (b.end.line - b.start.line == a.end.line - a.start.line) {
                if (b.end.col - b.start.col == a.end.col - a.start.col)
                    return a.start<b.start;
                else return b.end.col - b.start.col > a.end.col - a.start.col;
            } else return b.end.line - b.start.line > a.end.line - a.start.line;
        } else return b.end.file - b.start.file > a.end.file - a.start.file;
    }
};

/** Describes an Error, a Warning or auxiliary information for one of them.*/
struct ErrorElement
{
    /** Enumerates the type of diagnostic we can print.*/
    enum class EType {
        Error,
        Warning,
        TechnicalInfo
    };
    /** Enumerates the types individual lines of a diagnostic messages.*/
    enum class ELineType {
        Main,         ///<This is the main message. For single-line diagnostics, too
        Context,      ///<This is explanatory text to the main message
        Substitutuion ///<This is additional info on file inclusion or parameter substitution
    };
    FileLineCol relevant_line; ///<The location to which the information applies. This is shown.
    FileLineCol ordering_line; ///<The location used at ordering the errors. Different for auxiliary info.
    EType type;                ///<Error or warning or technical info
    ELineType line_type;       ///<Whether this is the main diagnostic message or supplementary info
    bool isOnlyOnce;           ///<True if messages with this text shall only be displayed once.
    std::string text;          ///<The full text of the error/warning including location, etc.
    std::string message;       ///<The original text of the error/warning.
    bool operator < (const ErrorElement &other) const noexcept {return ordering_line < other.ordering_line;}
    bool AppearsSame(const ErrorElement &o) const noexcept { return text==o.text; }  ///<Return true if the text of the two error is identical including line numbers (so we remove the redundant ones.)
    bool Fits(bool oWarnings, bool oTechnicalInfo) const noexcept { return type==ErrorElement::EType::Error || (type==ErrorElement::EType::Warning && oWarnings) || (type==ErrorElement::EType::TechnicalInfo && oTechnicalInfo); }
};

class Attribute;

/** Creates and stores errors/warnings.*/
class MscError {
protected:
    bool hadFatal = false;                        ///<Indicates if FatalError() was called. Such errors result in no chart being produced. Used to adjust command-line messages.
    std::vector<ErrorElement> Messages;           ///<Contains the error messages and their related auxiliary info
    bool IsInStore(const ErrorElement &e) const;
    bool unique_push_back(const ErrorElement &e) { if (IsInStore(e)) return false;  Messages.push_back(e); return true; } ///<Push back an element, but only if one with similar text does not exist.
    bool unique_push_back(ErrorElement &&e) { if (IsInStore(e)) return false;  Messages.push_back(std::move(e)); return true; } ///<Push back an element, but only if one with similar text does not exist.

    void Add(const FileLineCol &linenum, const FileLineCol& linenum_ord, std::string_view s, std::string_view once, ErrorElement::EType t, bool omit_error_warning, ErrorElement::ELineType lt = ErrorElement::ELineType::Main);
    bool AddOne(const FileLineCol& linenum, const FileLineCol& linenum_ord, std::string_view s, ErrorElement::EType t, bool is_once, bool add_parenthesis, bool omit_error_warning, ErrorElement::ELineType lt);
    void Add(const Attribute &a, bool atValue, std::string_view s, std::string_view once, ErrorElement::EType t);

public:
    std::vector<std::string> Files; ///<The list of files we parsed in
    bool warn_mscgen = false;       ///<True if we emit warnings for mscgen features we do not plan to support.
    MscError() noexcept = default;
    /** Adds a piece of technical info.
     * @param [in] linenum The location of the warning.
     * @param [in] s The message to display.*/
    void TechnicalInfo(FileLineCol linenum, std::string_view s)
        {Add(linenum, linenum, s, {}, ErrorElement::EType::TechnicalInfo, false);}
    unsigned AddFile(std::string_view filename);  ///<Add another file to the list of files.
    /** Adds a warning.
     * @param [in] linenum The location of the warning.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Warning(FileLineCol linenum, std::string_view s, std::string_view once = {})
        {Add(linenum, linenum, s, once, ErrorElement::EType::Warning, false);}
    /** Adds a warning auxiliary info.
     * @param [in] linenum The location of the warning to show.
     * @param [in] linenum_ord The location to use at ordering.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Warning(FileLineCol linenum, FileLineCol linenum_ord, const std::string &s, std::string_view once = {})
        {Add(linenum, linenum_ord, "("+s+")", once, ErrorElement::EType::Warning, true, ErrorElement::ELineType::Context);}
    /** Adds a warning for an attribute.
     * @param [in] a The faulty attribute.
     * @param [in] atValue True if the problem is with the value of the attr, false if with its name.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Warning(const Attribute &a, bool atValue, const std::string_view s, std::string_view once = {})
        {Add(a, atValue, s, once, ErrorElement::EType::Warning);}
    /** Adds an error.
     * @param [in] linenum The location of the error.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Error(FileLineCol linenum, std::string_view s, std::string_view once = {})
        {Add(linenum, linenum, s, once, ErrorElement::EType::Error, false);}
    /** Adds a error auxiliary info.
     * @param [in] linenum The location of the error to show.
     * @param [in] linenum_ord The location to use at ordering.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Error(FileLineCol linenum, FileLineCol linenum_ord, const std::string &s, std::string_view once = {})
        {Add(linenum, linenum_ord, "("+s+")", once, ErrorElement::EType::Error, true, ErrorElement::ELineType::Context);}
    /** Adds a error for an attribute.
     * @param [in] a The faulty attribute.
     * @param [in] atValue True if the problem is with the value of the attr, false if with its name.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void Error(const Attribute &a, bool atValue, std::string_view s, std::string_view once = {})
        {Add(a, atValue, s, once, ErrorElement::EType::Error);}
    /** Adds a fatal an error that prevented chart generation.
     * @param [in] linenum The location of the error.
     * @param [in] s The message to display
     * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void FatalError(FileLineCol linenum, std::string_view s, std::string_view once = {})
        {Add(linenum, linenum, s, once, ErrorElement::EType::Error, false); hadFatal=true;}
    /** Notifies of an mscgen feature (which is otherwise working, but we do not plan to maintain).
    * @param [in] linenum The location of the error.
    * @param [in] s The message to display
    * @param [in] once Auxiliary info, a type of hint on what to do. Added as a separate element, but only once.*/
    void WarnMscgen(FileLineCol linenum, std::string_view s, std::string_view once = {});
    /** Notifies of an mscgen attr/option (which is otherwise accepted, but we do not plan to maintain).
    * @param [in] a The faulty attribute.
    * @param [in] isOption True if this is a chart option, false if an attribute
    * @param [in] name The name of the attribute to use instead*/
    void WarnMscgenAttr(const Attribute &a, bool isOption, std::string_view name);

    void DeleteLastErrorAt(FileLineCol linenum) {
        while (Messages.size()
               && Messages.back().type == ErrorElement::EType::Error
               && Messages.back().ordering_line == linenum)
            Messages.pop_back();
    }

    /** Print all the errors (and warnings) onto a string*/
    std::string Print(bool oWarnings, bool oTechnicalInfo) const;
    /** True if there were errors added*/
    bool hasErrors() const { return Messages.end()!=std::ranges::find(Messages, ErrorElement::EType::Error, &ErrorElement::type); }
    /** True if there were fatal errors added*/
    bool hasFatal() const {return hadFatal;}
    /** Sort errors and warnings by location */
    void Sort();
    /** Returns the number of errors (includes warnings if oWarnings is true).*/
    unsigned GetErrorNum(bool oWarnings, bool oTechnicalInfo) const;
    /** Look up the Nth message, if we consider only error or warnings and/or tech info, as well.*/
    const ErrorElement *GetError(unsigned num, bool oWarnings, bool oTechnicalInfo) const;

    /** Delete the Nth message, if we consider only error or warnings and/or tech info, as well.*/
    void DeleteError(unsigned num, bool oWarnings, bool oTechnicalInfo)
    {
        auto err = GetError(num, oWarnings, oTechnicalInfo);
        if (err) Messages.erase(Messages.begin() + (err-&Messages.front()));
    }
    /** Get the location of an error */
    FileLineCol GetErrorLoc(unsigned num, bool oWarnings, bool oTechnicalInfo) const {
        auto p = GetError(num, oWarnings, oTechnicalInfo);
        return p ? p->relevant_line : FileLineCol();
    }
    /** Get the text of an error, including line numbers and all. */
    std::string_view GetErrorText(unsigned num, bool oWarnings, bool oTechnicalInfo) const {
        auto p = GetError(num, oWarnings, oTechnicalInfo);
        return p ? p->text : std::string_view{};
    }
    /** Get the message of an error, excluding line numbers and 'error' or 'warning'. */
    std::string_view GetErrorMessage(unsigned num, bool oWarnings, bool oTechnicalInfo) const
    {
        auto p = GetError(num, oWarnings, oTechnicalInfo);
        return p ? p->message : std::string_view{};
    }
    ErrorElement FormulateElement(const FileLineCol& linenum, const FileLineCol& linenum_ord, ErrorElement::EType t,
                                  bool is_once, bool add_para, bool omit_message_type, ErrorElement::ELineType lt,
                                  std::string_view msg) const;
    /** Clearing all collected errors and warnings, but keeps the files.*/
    void Clear() noexcept {Messages.clear(); hadFatal=warn_mscgen=false; }
    /** Copies both files and messages from another Error collection.
     * It asserts that our 'Files' member starts with all the files in the
     * object we merge in.*/
    MscError &operator+=(const MscError &);

    void RemovePathFromErrorMessages();
};

/** Describes how we shall approach mscgen compatibility.
 * Used by both Msc and Csh classes.*/
enum class EMscgenCompat
{
    NO_COMPAT=0,   ///<Never switch to compatibility mode
    AUTODETECT=1,  ///<Switch to compatibility mode if 'msc {...}' detected
    FORCE_MSCGEN=2 ///<Start in mscgen compatibility mode eve if no 'msc {...}' detected
};


inline FileLineColDiff::FileLineColDiff(const FileLineCol &t, const FileLineCol &o) :
        same_file(t.file==o.file && t.next==o.next), line(abs(int(t.line)-int(o.line))),
        col(t.line==o.line ? abs(int(t.col)-int(o.col)) :
            t.line < o.line ? int(o.col)-int(t.col) : int(t.col)-int(o.col)) {}

/** A function that opens a text file of utf8 name. Returns nullptr if file not found. */
typedef FILE* (OpenNamedFileFunction)(const char*, bool, bool); //write, binary

#endif
