#ifndef TOML11_SOURCE_LOCATION_IMPL_HPP
#define TOML11_SOURCE_LOCATION_IMPL_HPP

#include "../fwd/source_location_fwd.hpp"

#include "../color.hpp"
#include "../utility.hpp"
#include "../version.hpp"

#include <iomanip>
#include <sstream>
#include <string>
#include <vector>

#include <cctype>

namespace toml
{
inline namespace TOML11_INLINE_VERSION_NAMESPACE
{

TOML11_INLINE source_location::source_location(const detail::region& r)
    : is_ok_(false),
      first_line_(1),
      first_column_(1),
      first_offset_(1),
      last_line_(1),
      last_column_(1),
      last_offset_(1),
      length_(0),
      file_name_("unknown file")
{
    if(r.is_ok())
    {
        this->is_ok_        = true;
        this->file_name_    = r.source_name();
        this->first_line_   = r.first_line_number();
        this->first_column_ = r.first_column_number();
        this->last_line_    = r.last_line_number();
        this->last_column_  = r.last_column_number();
        this->length_       = r.length();

        const auto lines = r.as_lines();
        assert( ! lines.empty());

        for(const auto& l : lines)
        {
            this->line_str_.push_back(l.first);
        }

        this->first_offset_ = lines.at(             0).second + 1; // to 1-origin
        this->last_offset_  = lines.at(lines.size()-1).second + 1;
    }
}

TOML11_INLINE std::string const& source_location::first_line() const
{
    if(this->line_str_.size() == 0)
    {
        throw std::out_of_range("toml::source_location::first_line: `lines` is empty");
    }
    return this->line_str_.front();
}
TOML11_INLINE std::string const& source_location::last_line() const
{
    if(this->line_str_.size() == 0)
    {
        throw std::out_of_range("toml::source_location::first_line: `lines` is empty");
    }
    return this->line_str_.back();
}

namespace detail
{

TOML11_INLINE std::size_t integer_width_base10(std::size_t i) noexcept
{
    std::size_t width = 0;
    while(i != 0)
    {
        i /= 10;
        width += 1;
    }
    return width;
}

TOML11_INLINE std::ostringstream&
format_filename(std::ostringstream& oss, const source_location& loc)
{
    // --> example.toml
    oss << color::bold << color::blue << " --> " << color::reset
        << color::bold << loc.file_name() << '\n' << color::reset;
    return oss;
}

TOML11_INLINE std::ostringstream& format_empty_line(std::ostringstream& oss,
        const std::size_t lnw)
{
    //    |
    oss << detail::make_string(lnw + 1, ' ')
        << color::bold << color::blue << " |\n"  << color::reset;
    return oss;
}

TOML11_INLINE std::ostringstream& format_line(std::ostringstream& oss,
    const std::size_t lnw, const std::size_t linenum, const std::string& line)
{
    // 10 | key = "value"
    oss << ' ' << color::bold << color::blue
        << std::setw(static_cast<int>(lnw))
        << std::right << linenum << " | "  << color::reset;
    for(const char c : line)
    {
        if(std::isgraph(c) || c == ' ')
        {
            oss << c;
        }
        else
        {
            oss << show_char(c);
        }
    }
    oss << '\n';
    return oss;
}
TOML11_INLINE std::ostringstream& format_underline(std::ostringstream& oss,
        const std::size_t lnw, const std::size_t col, const std::size_t len,
        const std::string& msg)
{
    //    |       ^^^^^^^-- this part
    oss << make_string(lnw + 1, ' ')
        << color::bold << color::blue << " | " << color::reset;

    // in case col is 0, so we don't create a string with size_t max length
    const std::size_t sanitized_col = col == 0 ? 0 : col - 1 /*1-origin*/;
    oss << make_string(sanitized_col, ' ')
        << color::bold << color::red
        << make_string(len, '^') << "-- "
        << color::reset << msg << '\n';

    return oss;
}

TOML11_INLINE std::string format_location_impl(const std::size_t lnw,
    const std::string& prev_fname,
    const source_location& loc, const std::string& msg)
{
    std::ostringstream oss;

    if(loc.file_name() != prev_fname)
    {
        format_filename(oss, loc);
        if( ! loc.lines().empty())
        {
            format_empty_line(oss, lnw);
        }
    }

    if(loc.lines().size() == 1)
    {
        // when column points LF, it exceeds the size of the first line.
        std::size_t underline_limit = 1;
        if(loc.first_line().size() < loc.first_column_offset())
        {
            underline_limit = 1;
        }
        else
        {
            underline_limit = loc.first_line().size() - loc.first_column_offset() + 1;
        }
        const auto underline_len = (std::min)(underline_limit, loc.length());

        format_line(oss, lnw, loc.first_line_number(), loc.first_line());
        format_underline(oss, lnw, loc.first_column_offset(), underline_len, msg);
    }
    else if(loc.lines().size() == 2)
    {
        const auto first_underline_len =
            loc.first_line().size() - loc.first_column_offset() + 1;
        format_line(oss, lnw, loc.first_line_number(), loc.first_line());
        format_underline(oss, lnw, loc.first_column_offset(),
                first_underline_len, "");

        format_line(oss, lnw, loc.last_line_number(), loc.last_line());
        format_underline(oss, lnw, 1, loc.last_column_offset(), msg);
    }
    else if(loc.lines().size() > 2)
    {
        const auto first_underline_len =
            loc.first_line().size() - loc.first_column_offset() + 1;
        format_line(oss, lnw, loc.first_line_number(), loc.first_line());
        format_underline(oss, lnw, loc.first_column_offset(),
                first_underline_len, "and");

        if(loc.lines().size() == 3)
        {
            format_line(oss, lnw, loc.first_line_number()+1, loc.lines().at(1));
            format_underline(oss, lnw, 1, loc.lines().at(1).size(), "and");
        }
        else
        {
            format_line(oss, lnw, loc.first_line_number()+1, " ...");
            format_empty_line(oss, lnw);
        }
        format_line(oss, lnw, loc.last_line_number(), loc.last_line());
        format_underline(oss, lnw, 1, loc.last_column_offset(), msg);
    }
    // if loc is empty, do nothing.
    return oss.str();
}

} // namespace detail
} // TOML11_INLINE_VERSION_NAMESPACE
} // toml
#endif // TOML11_SOURCE_LOCATION_IMPL_HPP
