/*
    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.cpp The definition of classes for error registration.
 * @ingroup libcgencommon_files */

#define _CRT_SECURE_NO_WARNINGS

#include <algorithm>
#include <cstring>
#include "error.h"
#include "utf8utils.h"
#include "stringparse.h"

/** Pushes a new location onto our stack.*/
void FileLineCol::Push(int a, size_t b, size_t c, EInclusionReason r)
{
    std::shared_ptr<FileLineCol> p = std::make_shared<FileLineCol>(file, line, col, reason);
    p->next = std::move(next);
    next = p;
    file = a;
    line = b;
    col = c;
    reason = r;
}

/** Pops the stack, creating a modifiable copy of what is below us.
 * Returns false if we were top.*/
bool FileLineCol::Pop() noexcept
{
    if (!next) return false;
    file = next->file;
    line = next->line;
    col = next->col;
    reason = next->reason;
    std::shared_ptr<const FileLineCol> tmp_next = next->next; //to keep next->next from disappearing due to the delete below.
    next = std::move(tmp_next);
    return true;
}

/** Advance the position (creating a new position object) with the UTF8 text's length.
 * This is best called if the string is the same that starts at the origial location
 * in the input file to get the next char position after the string.
 * If a '\n', '\0xa\0xd' or a is found we bump line and reset col.
 * We do not test if a character is valid utf8 or not. We ignore truncated
 * utf-8 characters at the end - what we return will be the pos of that truncated char.
 * If we encounter a zero character, we return the position of that.*/
FileLineCol FileLineCol::AdvanceUTF8(std::string_view utf8) const noexcept
{
    FileLineCol ret(*this);
    for (auto i = utf8.begin(); i!=utf8.end(); /*nope*/) {
        if (*i=='\x0d') {
            if (std::next(i)!=utf8.end() && *std::next(i)=='\x0a')
                i += 2;
            else
single_char_newline:
                i++;
            ret.line++;
            ret.col = 1;
            continue;
        }
        if (*i=='\x0a') goto single_char_newline;
        if (*i==0) return ret;
        int length = UTF8TrailingBytes(*i)+1;
        if (utf8.end() - i < length)
            return ret;
        i += length;
        ret.col++;
    }
    return ret;
}


/** Prints a file location as an escape string. We include the whole stack including reason.*/
std::string FileLineCol::Print() const
{
    std::string ret("\\" ESCAPE_STRING_LOCATION "(");
    for (const FileLineCol *p = this; p; p = p->next ? p->next.get() : nullptr)
        ret << p->file << "," << p->line << "," << p->col << "," << unsigned(p->reason) << ";";
    ret.back() = ')';
    return ret;
}

/** Reads a printed file location.
 * On success we set our value to that location, set 's' to the first char
 * after us and return true. On failure, we leave us and 's' unchanged and
 * return false. */
bool FileLineCol::Read(std::string_view &s)
{
    if (s.length()<4) return false; //at lest "\x01()"
    if (s[0]!='\\' || s[1]!=ESCAPE_CHAR_LOCATION || s[2]!='(')
        return false;
    const size_t end = s.find_first_of(')');
    if (end==std::string_view::npos)
        return false;
    auto ss = s.substr(3, end-3); //the content within the parameter only
    std::shared_ptr<FileLineCol> ret;
    FileLineCol* res = nullptr;
    while(1) {
        const size_t segment_end = ss.find_first_of(';');
        std::string data(ss.substr(0, segment_end));
        unsigned u;

        if (auto p = std::make_shared<FileLineCol>(); res) {
            FileLineCol* pp = p.get();
            res->next = std::move(p);
            res = pp;
        } else {
            ret = std::move(p);
            res = ret.get();
        }
        unsigned l, c;
        if (4!=sscanf(data.c_str(), "%d,%d,%d,%d", &res->file, &l, &c, &u))
            return false; //ret will be destroyed and the whole chain with it.
        res->line = l;
        res->col = c;
        res->reason = EInclusionReason(u);
        if (segment_end==std::string_view::npos) break;
        ss.remove_prefix(segment_end+1);
    };
    *this = std::move(*ret);
    s.remove_prefix(end+1);
    return true;
}

//returns -1 if the two ranges do not even touch
inline int _overlap(unsigned start, unsigned end, unsigned ostart, unsigned oend)
{
    if (start > oend || end < ostart) return -1;
    return std::min(end, oend) - std::max(start, ostart);
}


unsigned MscError::AddFile(std::string_view filename)
{
    for (unsigned i=0; i<Files.size(); i++)
        if (Files[i] == filename) return i;
    Files.emplace_back(filename);
    return (unsigned)Files.size()-1;
}

/** Formulate an error element - used internally.
 * @param [in] linenum The line number to show. We do not consider the series (the next member), just the file, line and col members.
 * @param [in] linenum_ord The line number to use at ordering. Here we consider all parts.
 * @param [in] t The type of the message (error, warning, info, etc.)
 * @param [in] is_once True if the message is to be displayed only once.
 * @param [in] add_para Add parenthesis around
 * @param [in] omit_message_type If true, we do not label the message as 'warning' or 'error'.
 * @param [in] lt If this is the main message or explanatory context for a previous main message.
 * @param [in] msg The message to display
 * @returns The error element. */
ErrorElement MscError::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
{
    ErrorElement e;
    e.message = msg;
    //replace hidden escapes to the '$$' symbol denoting a value unique per proc invocation
    StringFormat::ReplaceHiddenEscapes(e.message, false, "$$");
    if (linenum.file>=0) { //only do this for valid linenums
        if (linenum.file<int(Files.size()) && Files[linenum.file].length()>0)
            e.text = Files[linenum.file];
        if (linenum.line) {
            if (e.text.length()>0) e.text.append(":");
            e.text << linenum.line;
            if (linenum.col)
                e.text << ":" << linenum.col;
        }
    }
    if (e.text.length()>0) e.text.append(": ");
    if (add_para)
        e.message = "("+e.message+")";
    else if (!omit_message_type) switch (t) {
    case ErrorElement::EType::Error: e.text.append("error: "); break;
    case ErrorElement::EType::Warning: e.text.append("warning: "); break;
    case ErrorElement::EType::TechnicalInfo: e.text.append("info: "); break;
    default: _ASSERT(0);
    }
    e.text.append(e.message);
    e.relevant_line = linenum;
    e.ordering_line = linenum_ord;
    e.type = t;
    e.line_type = lt;
    e.isOnlyOnce = is_once;
    return e;
}

/**Adds an error/warning related to an attribute.
 *
 * Line numbers are deducted from the 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.
 * @param [in] t The type of the message (error, warning, info, etc.) */
 void MscError::Add(const Attribute &a, bool atValue, std::string_view s, std::string_view once, ErrorElement::EType t)
{
    if (a.error) return;
    a.error = true;
    if (atValue)
        Add(a.linenum_value.start, a.linenum_value.start, s, once, t, false);
    else
        Add(a.linenum_attr.start, a.linenum_attr.start, s, once, t, false);
}

/**Adds an error/warning with a potential explanatory text that should be shown only once.
 * Both the message and the explanatory text may result in multiple lines if there is file inclusion.
 * @param [in] linenum The line number to show.
 * @param [in] linenum_ord The line number 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.
 * @param [in] t The type of the message (error, warning, info, etc.)
 * @param [in] omit_error_warning If true, we do not label the message as 'warning' or 'error'.
 * @param [in] lt How to interpret this message. This allows adding a Context or Substitution line manually.*/
void MscError::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)
{
    bool inserted = AddOne(linenum, linenum_ord, s, t, false, false, omit_error_warning, lt);
    if (inserted && once.length()>0)
        AddOne(linenum, linenum_ord, once, t, true, true, omit_error_warning,
               lt == ErrorElement::ELineType::Main ? ErrorElement::ELineType::Context : lt);
}

/**Adds an error/warning message.
 * The message may result in multiple lines if there is file inclusion.
 * @param [in] linenum The line number to show.
 * @param [in] linenum_ord The line number to use at ordering.
 * @param [in] s The message to display
 * @param [in] t The type of the message (error, warning, info, etc.)
 * @param [in] is_once True if the message is to be displayed only once.
 * @param [in] add_parenthesis If set this message is enclosed in parenthesis.
 * @param [in] omit_error_warning If true, we do not label the message as 'warning' or 'error'.
 * @param [in] lt If this is the main message or explanatory context for a previous main message.
 * @returns True if we have actually inserted something.*/
bool MscError::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)
{
    ErrorElement e1 = FormulateElement(linenum, linenum_ord, t, is_once, add_parenthesis, omit_error_warning, lt, s);
    if (!unique_push_back(std::move(e1))) return false;
    bool was_param = false;
    for (const FileLineCol *p = &linenum; p->GetNext(); p = p->GetNext()) {
        switch (p->reason) {
        default:
        case EInclusionReason::TOP_LEVEL: _ASSERT(0); continue;
        case EInclusionReason::PARAMETER:
            e1 = FormulateElement(*p->GetNext(), linenum_ord, t, false, true, true, ErrorElement::ELineType::Substitutuion,
                                  "...when the procedure parameter is substituted here.");
            was_param = true;
            break;
        case EInclusionReason::PROCEDURE:
            //Omit this if we have just shown a parameter
            if (was_param) {
                was_param = false;
                continue; //Do not list error at parameter invocation, then at parameter use - and then the procedure invocation again
            }
            e1 = FormulateElement(*p->GetNext(), linenum_ord, t, false, true, true, ErrorElement::ELineType::Substitutuion,
                                  "...when the procedure is invoked from here.");
            break;
        case EInclusionReason::INCLUDE:
            e1 = FormulateElement(*p->GetNext(), linenum_ord, t, false, true, true, ErrorElement::ELineType::Substitutuion,
                                  "...included from here.");
            was_param = false;
            break;
        case EInclusionReason::COPY:
            e1 = FormulateElement(*p->GetNext(), linenum_ord, t, false, true, true, ErrorElement::ELineType::Substitutuion,
                                  "...when copied from here.");
            was_param = false;
            break;
        }
        Messages.push_back(std::move(e1));
    }
    return true;
}

/** Check if an error is in one of the error stores.*/
bool MscError::IsInStore(const ErrorElement & e) const
{
    return Messages.end()!=std::find_if(Messages.begin(), Messages.end(),
        [&e](const auto &a) {return a.AppearsSame(e); });
}

void MscError::Sort()
{
    if (Messages.size()<2) return;
    sort(Messages.begin(), Messages.end());
    for (std::vector<ErrorElement>::size_type i=Messages.size()-1; i>0; i--)
        if (Messages[i].isOnlyOnce)
            for (std::vector<ErrorElement>::size_type j=i; j>0; j--)
                if (Messages[j-1].isOnlyOnce && Messages[i].message==Messages[j-1].message) {
                    Messages.erase(Messages.begin()+i);
                    break;
                }
}

unsigned MscError::GetErrorNum(bool oWarnings, bool oTechnicalInfo) const
{
    return (unsigned) std::count_if(Messages.begin(), Messages.end(),
                                    [oWarnings, oTechnicalInfo](auto& m)
                                    {return m.Fits(oWarnings, oTechnicalInfo); });
}

const ErrorElement *MscError::GetError(unsigned num, bool oWarnings, bool oTechnicalInfo) const
{
    for (auto &m : Messages)
        if (m.Fits(oWarnings, oTechnicalInfo))
            if (0==num--) return &m;
    _ASSERT(0);
    return nullptr;
}

void MscError::WarnMscgen(FileLineCol linenum, std::string_view s, std::string_view once)
{
    if (warn_mscgen)
        Add(linenum, linenum, StrCat(s, " (This is an mscgen syntax.)"), once, ErrorElement::EType::Warning, false);
}

void MscError::WarnMscgenAttr(const Attribute &a, bool isOption, std::string_view name)
{
    string ao = isOption ? "Option" : "Attribute";
    if (warn_mscgen)
        Add(a.linenum_attr.start, a.linenum_attr.start,
            ao << " is according to mscgen syntax. Use '" << name << "' instead.",
            "", ErrorElement::EType::Warning, false);
}


string MscError::Print(bool oWarnings, bool oTechnicalInfo) const
{
    string a;
    for (auto &m : Messages)
        if (m.Fits(oWarnings, oTechnicalInfo))
            a.append(m.text).append("\n");
    return a;
}


MscError& MscError::operator += (const MscError& o)
{
    //Ensure that o.Files is a prefix of 'Files'=>all locations in 'o' are valid in 'this'
    _ASSERT(std::ranges::mismatch(Files, o.Files).in2 == o.Files.end());
    hadFatal |= o.hadFatal;
    warn_mscgen |= o.warn_mscgen;
    std::ranges::copy(o.Messages, std::back_inserter(Messages));
    return *this;
}

/** If any message we print starts with any of the filenames in Files,
 * remove its path portion.*/
void MscError::RemovePathFromErrorMessages()
{
    for (auto &fn : Files)
        if (const size_t p = fn.find_last_of("\\/"); p!=std::string::npos)
            for (auto& ee: Messages)
                if (ee.text.starts_with(fn))
                    ee.text.erase(0, p+1);
}
