#pragma once
#include <type_traits>
#include <tuple>
#include <iostream>
#include "csh.h"
#include "error.h"
#include "utf8utils.h"
#include "stringparse.h"

#define INLINE_OPEN_STR "<<<"
#define INLINE_CLOSE_STR ">>>"

template <bool CSH>
class sv_reader;

//We do not use a FileLineColRange as location type for compile parse,
//because copying/destroying it many times is expensive due to the
//shared pointer inside (atomic increments/decrements).
//Instead we use only the line/column of the current file.
//We also store a pointer to the input stream, from which we can form
//full FileLineCol and FileLineColRange objects.
struct LineCol {
    size_t line = 0; ///<The line of the location
    size_t col = 0;  ///<The character inside the line. First char is index zero.
    const sv_reader<false>* input = nullptr;
    constexpr auto operator <=>(const LineCol&) const noexcept = default;
};

struct LineColRange {
    LineCol from, till;
    FileLineCol start() const noexcept;
    FileLineCol end() const noexcept;
    FileLineCol after() const noexcept;
    FileLineColRange range() const noexcept;
    FileLineColRange IncStartCol() const noexcept { auto ret = range(); ret.start.col++; return ret; }
    LineColRange operator + (const LineColRange& o) const noexcept {
        return {std::min(from, o.from), std::max(till, o.till)};
    }
    operator FileLineCol() const noexcept { return start(); }
    operator FileLineColRange() const noexcept { return range(); }
};

template <bool CSH>
struct SourceLocation {
    std::conditional<CSH, CharByteIndex, LineCol>::type pos;
    //If text is not null, we check for newlines.
    //Standalone '\ n' and '\ r' count as newline, but combined
    // '\ n\ r' counts as a single one.
    template <bool may_contain_newline>
    void advance(std::string_view text) noexcept {
        /* convert token length from byte-length to character-length */
        if constexpr (CSH) {
            pos.char_index += UTF8len(text);
            pos.byte_index += text.length();
        } else if constexpr (!may_contain_newline)
            pos.col += UTF8len(text);
        else for (const char *p = text.data(), * const end = text.data() + text.size(); p < end; ++p, ++pos.col)
            if (*p == '\x0a') {
                pos.col = 0; //will get incremented immediately
                pos.line++;
            } else if (*p == '\x0d') {
                if (p + 1 < end && p[1] == '\x0a')
                    ++p;
                pos.col = 0; //will get incremented immediately
                pos.line++;
            } else
                p += UTF8TrailingBytes(*p);
    }
};

template <bool CSH>
struct SourceLocationRange : std::conditional<CSH, CshPos, LineColRange>::type {
    void merge(const SourceLocationRange<CSH>& rhs) noexcept {
        if constexpr (CSH)
            this->end_pos = rhs.end_pos;
        else
            this->till = rhs.till;
    }
    friend std::ostream& operator<<(std::ostream& os, const SourceLocationRange<CSH>& loc) {
        if constexpr (CSH) {
            os << '(' << loc.first_pos.char_index;
            if (loc.first_pos.char_index != loc.first_pos.byte_index)
                os << "/" << loc.first_pos.byte_index;
            os << "->" << loc.end_pos.char_index;
            if (loc.end_pos.char_index != loc.end_pos.byte_index)
                os << "/" << loc.end_pos.byte_index;
            return os << ')';
        } else
            return os << '(' << loc.from.line << ':' << loc.from.col << "->" << loc.till.line << ':' << loc.till.col << ')';
    }
    void set(const SourceLocation<CSH>& lhs, const SourceLocation<CSH>& rhs) noexcept {
        if constexpr (CSH) {
            this->first_pos = lhs.pos;
            this->end_pos = rhs.pos;
        } else {
            this->from = lhs.pos;
            this->till = rhs.pos;
        }
    }
    //Creates an empty range at 'p'
    void set(const SourceLocation<CSH>& p) noexcept { return set(p, p); }

    /** Returns the position of the first character in the range (or that of the one after if the range is empty.*/
    SourceLocation<CSH> get_begin() const noexcept { if constexpr (CSH) return {this->first_pos}; else return {this->from}; }
    /** Returns the position of the first character after the range */
    SourceLocation<CSH> get_end() const noexcept { if constexpr (CSH) return {this->end_pos}; else return {this->till}; }
};

//This will be needed by bison parsers. It manipulates the
//location and works with SourceLocation<CSH>.
#define YYLLOC_DEFAULT(Current, Rhs, N)                 \
    do                                                  \
        if (N) {                                        \
            (Current) = YYRHSLOC (Rhs, 1);              \
            (Current).merge(YYRHSLOC (Rhs, N));         \
        } else                                          \
            (Current).set(YYRHSLOC (Rhs, 0).get_end()); \
    while (/*CONSTCOND*/ false)

template <bool CSH>
class sv_reader
{
    struct file_status {
        std::string_view text;
        SourceLocation<CSH> pos;
    };
    struct file_status_with_pos : file_status {//used only for compile parse (CSH=false)
        FileLineCol file_pos = {}; //the line & col members are not used (we use file_status::pos instead). This is only to store the file and the location stack.
    };
    using file = std::conditional<CSH, file_status, file_status_with_pos>::type;
    file buffer;                     //The current file we read
    std::optional<file_status> prev; //The state of the buffer before the previous commit (only in same file as 'buffer')
    std::vector<file> subsequent;    //Files that have included the current one
    friend LineColRange;
    std::string_view last_token;
public:
    /** Sets up the reader for parsing.
     * @param [in] text The text we need to parse. May only be a fragment of a file on disc,
     * e.g., for inlined charts.
     * @param [in] first_char The position of the first character of 'text'.
     *   - for CSH parse this is an integer, the 1-based UTF-8 character (not byte) index. 
     *     If 'text' is a whole file, it shall be 1.
     *   - for regular parse this is a FileLineCol, which should be line=1, col=1 if 'text'
     *     is a full file. */
    sv_reader(std::string_view text, std::conditional<CSH, CharByteIndex, const FileLineCol&>::type first_char) noexcept {
        buffer.text = text;
        if constexpr (CSH) {
            buffer.pos = { first_char };
        } else {
            buffer.pos = SourceLocation<CSH>{ LineCol{.line = first_char.line, .col = first_char.col, .input = this } };
            buffer.file_pos = first_char;
        }
    }

    //One token cannot span multiple files, so we can ignore
    //subsequent here.
    char peek(size_t i) const { return buffer.text[i]; }
    bool has(size_t len) const { return buffer.text.size() >= len; }
    std::string_view view(size_t i) const { return buffer.text.substr(0, i); }
    std::string_view view() const { return buffer.text; }
    std::string_view get_last_token() const noexcept { return last_token; }
    const SourceLocation<CSH>& current_pos() const noexcept { return buffer.pos; }
    template <bool may_contain_newline, bool need_rollback>
    SourceLocationRange<CSH> commit(size_t len) {
        if constexpr (!CSH && need_rollback)
            prev = buffer;
        last_token = buffer.text.substr(0, len);
        auto start = buffer.pos;
        buffer.pos.template advance<may_contain_newline>(buffer.text.substr(0, len));
        auto end = buffer.pos;
        buffer.text.remove_prefix(len);
        SourceLocationRange<CSH> ret;
        ret.set(std::move(start), std::move(end));
        return ret;
    }
    //Rolls the last commit back. Possible only if within the same file.
    //@returns true on success
    bool rollback() requires (!CSH) {
        _ASSERT(prev);
        static_cast<file_status&>(buffer) = *prev;
        prev.reset();
        return true;
    }
    void push(std::string_view text, EInclusionReason reason, const FileLineCol& new_pos, FileLineCol&& old_pos) requires (!CSH) {
        _ASSERT(old_pos.file == buffer.file_pos.file);
        buffer.file_pos = std::move(old_pos);
        subsequent.push_back(buffer);
        buffer.text = text;
        buffer.pos.pos.line = new_pos.line;
        buffer.pos.pos.col = new_pos.col;
        buffer.file_pos.Push(new_pos.file, new_pos.line, new_pos.col, reason);
    }
    void try_pop() {
        if constexpr (!CSH)
            if (buffer.text.empty() && subsequent.size()) {
                buffer = std::move(subsequent.back());
                subsequent.pop_back();
            }
    }
};

inline FileLineCol LineColRange::start() const noexcept {
    FileLineCol ret = from.input->buffer.file_pos;
    ret.line = from.line;
    ret.col = from.col;
    return ret;
}
inline FileLineCol LineColRange::end() const noexcept {
    FileLineCol ret = till.input->buffer.file_pos;
    ret.line = till.line;
    ret.col = till.col;
    return ret;
}
inline FileLineCol LineColRange::after() const noexcept {
    FileLineCol ret = till.input->buffer.file_pos;
    ret.line = till.line;
    ret.col = till.col;
    return ret;
}

inline FileLineColRange LineColRange::range() const noexcept {
    FileLineColRange ret{from.input->buffer.file_pos, till.input->buffer.file_pos};
    ret.start.line = from.line;
    ret.start.col = from.col;
    ret.end.line = till.line;
    ret.end.col = till.col;
    return ret;
}


inline void remove_head_tail_whitespace(std::string_view& sv) noexcept {
    while (sv.size() && (sv.front() == ' ' || sv.front() == '\t'))
        sv.remove_prefix(1);
    while (sv.size() && (sv.back() == ' ' || sv.back() == '\t'))
        sv.remove_suffix(1);
}

/** Preprocess a colon-initiated label.
 * If 'two_colons' is true, the label starts with two colons (graph language)
 * We do all the following:
 * - Remove heading and trailing whitespace.
 * - Replace any internal CR or CRLF (and surrounding whitespaces) to "\n".
 * - Remove comments between # and line-end, except if # is preceded by an odd
 *   number of backslashes.
 * - Insert \0x2(file,line,col,reason;...) escapes where needed we changed the length of the
 *   preceding string, so that if we generate an error to any escapes thereafter
 *   those will have the right location in the input file. To cater for multiple inclusions
 *   (e.g., procedure replays) we include a FileLineCol, which may be more than one level.
 * All the while take potential utf-8 characters into account: we count characters, not bytes in location.
 *
 * The function copies the result to new memory and the caller shall free().*/
inline std::string process_colon_string(std::string_view s, FileLineCol loc, bool two_colons=false)
{
    std::string ret;
    const size_t original_line = loc.line;
    int old_pos = two_colons ? 2 : 1; //actually s begins with the colon(s), we skip that
    loc.col += old_pos;

    while (true) {
        //the current line begins at old_pos
        int end_line = old_pos;
        int start_line = old_pos;
        //find the end of the line
        while (end_line<(int)s.size() && s[end_line] != 10 && s[end_line] != 13)
            end_line++;
        //store the last char of the line to see how we ended. 0 for final end
        const char ending = (int)s.size() <= end_line ? 0 : s[end_line];
        //skip over the heading whitespace at the beginning of the line
        while (start_line<(int)s.length() && (s[start_line] == ' ' || s[start_line] == '\t'))
            start_line++;
        //find the last non-whitespace in the line
        int term_line = end_line - 1;
        //term_line can be smaller than start_line here if line is empty
        while (term_line >= start_line && (s[term_line] == ' ' || s[term_line] == '\t'))
            term_line--;
        //Generate a \l(file,line,col) escape and append
        //We have only stepped over whitespace, no UTF-8 characters: we can just add "start_line-old_pos"
        ret += FileLineCol(loc, loc.line, loc.col + (start_line - old_pos)).Print();
        //now append the line (without the whitespaces)
        //but remove anything after a potential #
        bool wasComment = false;
        bool emptyLine = true;
        while (start_line <= term_line) {
            //count number of consecutive \s
            unsigned num_of_backsp = 0;
            while (start_line <= term_line && s[start_line] != '#') {
                if (s[start_line] == '\\') num_of_backsp++;
                else num_of_backsp = 0;
                ret += s[start_line++];
                emptyLine = false;
            }
            //if we hit a # leave rest of line only if not preceded by even number of \s
            if (start_line<(int)s.size() && s[start_line] == '#') {
                if (num_of_backsp % 2) {
                    ret += s[start_line++]; //step over escaped #
                    emptyLine = false;
                } else {
                    wasComment = true;
                    break;
                }
            }
        }
        //We may have copied spaces before the comment, we skip those
        while (ret.length() && (ret.back() == ' ' || ret.back() == '\t'))
            ret.pop_back();
        //if ending was a null we are done with processing all lines
        if (ending==0) {
            //consider the utf-8 characters
            //end_line may equal old_pos, if the string is empty
            if (end_line > old_pos)
                loc.col += UTF8len(s.substr(old_pos, end_line - old_pos));
            break;
        }
        //append ESCAPE_CHAR_SOFT_NEWLINE escape for msc-generator,
        //but only if not an empty first line
        //append "\" + ESCAPE_CHAR_SOFT_NEWLINE if line ended with odd number of \s
        if (!emptyLine || original_line != loc.line) {
            //add a space for empty lines, if line did not contain a comment
            if (emptyLine && !wasComment)
                ret += ' ';
            //test for how many \s we have at the end of the line
            int pp = (int)ret.length() - 1;
            while (pp >= 0 && ret[pp] == '\\') pp--;
            //if odd, we insert an extra '\' to keep lines ending with \s
            if ((ret.length() - pp) % 2 == 0) ret += '\\';
            ret += "\\" ESCAPE_STRING_SOFT_NEWLINE;
        }
        //Check for a two character CRLF, skip over the LF, too
        if (ending == 13 && s[end_line + 1] == 10) end_line++;
        old_pos = end_line + 1;

        //Now advance loc
        loc.line++;
        loc.col = 1;
    }
    return ret;
}
