#include "BufferView.h"
#include <list>
#include <utility>
#include <limits.h>

BufferView::BufferView(std::shared_ptr<Buffer> buffer,
        std::shared_ptr<Buffer::Iterator> iterator,
        CharacterWidthDeterminer & character_width_determiner)
    : m_buffer(buffer),
      m_iterator(iterator),
      m_character_width_determiner(character_width_determiner)
{
    m_width = 1;
    m_height = 1;
    m_scroll_offset = 0;
    m_cursor_screen_row = 0;
    m_cursor_screen_column = 0;
    m_cursor_virtual_column = 0;
    m_cursor_row_offset = 0;
    m_rows_in_cursor_line = 0;
    m_target_screen_column = 0;
    m_target_virtual_column = 0;
    m_update_target_column = false;
}

void BufferView::resize(int width, int height)
{
    m_width = std::max(1, width);
    m_height = std::max(1, height);
}

void BufferView::set_scroll_offset(int scroll_offset)
{
    m_scroll_offset = std::max(0, scroll_offset);
}

BufferView::Iterator BufferView::vert_iter()
{
    return Iterator(*this);
}

void BufferView::update()
{
    /* Calculate number of rows in the cursor line and update cursor row offset
     * and cursor column values. */
    auto start_of_line = std::make_shared<Buffer::Iterator>(*m_iterator);
    start_of_line->go_start_of_line();
    m_rows_in_cursor_line = calculate_rows_in_cursor_line(start_of_line);

    /* Limit the cursor screen row taking into account view dimensions,
     * available buffer contents above and below the current line, and scroll
     * offset. */
    std::list<std::pair<int, std::shared_ptr<Buffer::Iterator>>> backward_lines;
    int so = effective_scroll_offset();
    int rows_above = screen_rows_above_line(start_of_line, backward_lines) + m_cursor_row_offset;
    int rows_below = screen_rows_below_line(start_of_line) + std::max(0, m_rows_in_cursor_line - m_cursor_row_offset - 1);
    int min_rows_to_leave_above = std::min(rows_above, so);
    int min_rows_to_leave_below = std::min(rows_below, so);
    m_cursor_screen_row = std::min(m_height - min_rows_to_leave_below - 1, m_cursor_screen_row);
    m_cursor_screen_row = std::max(min_rows_to_leave_above, m_cursor_screen_row);
    m_cursor_screen_row = std::max(m_height - rows_below - 1, m_cursor_screen_row);
    m_cursor_screen_row = std::min(rows_above, m_cursor_screen_row);

    /* Determine the first line that is visible in this view. */
    auto line_iterator = std::make_shared<Buffer::Iterator>(*m_iterator);
    int row_offset = m_cursor_screen_row - m_cursor_row_offset;
    if (row_offset <= 0)
    {
        line_iterator->go_start_of_line();
    }
    else for (auto rows_iterator_pair : backward_lines)
    {
        row_offset -= rows_iterator_pair.first;
        line_iterator = rows_iterator_pair.second;
        if (row_offset <= 0)
        {
            break;
        }
    }

    /* Now start with first visible line and build up all lines visible in the view. */
    m_lines.clear();
    while ((row_offset < m_height) && (line_iterator->valid()))
    {
        LineDescriptor ld;
        ld.row_offset = row_offset;
        ld.n_rows = calculate_rows_in_line(line_iterator);
        ld.line = std::make_shared<Buffer::Iterator>(*line_iterator);
        m_lines.push_back(ld);
        row_offset += ld.n_rows;
        if (!line_iterator->go_next_line())
            break;
    }

    /* Reset some fields if buffer becomes empty. */
    if (!m_iterator->valid())
    {
        m_cursor_screen_row = 0;
        m_cursor_screen_column = 0;
        m_cursor_virtual_column = 0;
    }
}

bool BufferView::cursor_move(CursorMovement which, uint32_t c, bool allow_eol)
{
    bool moved = false;

    switch (which)
    {
    case CursorMovement::LEFT:
        moved = m_iterator->go_left_in_line();
        m_update_target_column = true;
        break;
    case CursorMovement::RIGHT:
        moved = m_iterator->go_right_in_line(allow_eol);
        m_update_target_column = true;
        break;
    case CursorMovement::UP:
        moved = m_iterator->go_previous_line();
        break;
    case CursorMovement::DOWN:
        moved = m_iterator->go_next_line();
        break;
    case CursorMovement::SOL:
        moved = m_iterator->go_start_of_line();
        m_target_screen_column = 0;
        m_target_virtual_column = 0;
        break;
    case CursorMovement::EOL:
        moved = m_iterator->go_end_of_line(allow_eol);
        m_target_screen_column = INT_MAX;
        m_target_virtual_column = INT_MAX;
        break;
    case CursorMovement::FIRST_LINE:
        {
            auto it = m_buffer->begin();
            if (it != *m_iterator)
            {
                *m_iterator = it;
                moved = true;
            }
        }
        m_target_screen_column = 0;
        m_target_virtual_column = 0;
        break;
    case CursorMovement::LAST_LINE:
        {
            auto it = m_buffer->end();
            it.go_back();
            it.go_start_of_line();
            if (it != *m_iterator)
            {
                *m_iterator = it;
                moved = true;
            }
        }
        m_target_screen_column = 0;
        m_target_virtual_column = 0;
        break;
    case CursorMovement::SCREEN_ROW_UP:
        moved = move_cursor_screen_row_up(allow_eol);
        break;
    case CursorMovement::SCREEN_ROW_DOWN:
        moved = move_cursor_screen_row_down(allow_eol);
        break;
    case CursorMovement::FORWARD_UP_TO_CHAR:
        moved = move_cursor_forward_up_to_char(c);
        break;
    case CursorMovement::FORWARD_ON_TO_CHAR:
        moved = move_cursor_forward_on_to_char(c);
        break;
    case CursorMovement::BACK_UP_TO_CHAR:
        moved = move_cursor_backward_up_to_char(c);
        break;
    case CursorMovement::BACK_ON_TO_CHAR:
        moved = move_cursor_backward_on_to_char(c);
        break;
    }
    if (moved)
    {
        switch (which)
        {
        case CursorMovement::LEFT:
        case CursorMovement::RIGHT:
        case CursorMovement::SCREEN_ROW_UP:
        case CursorMovement::SCREEN_ROW_DOWN:
            determine_new_cursor_screen_row();
            break;
        case CursorMovement::UP:
        case CursorMovement::DOWN:
            move_forward_to_column(m_target_virtual_column, allow_eol);
            determine_new_cursor_screen_row();
            break;
        case CursorMovement::SOL:
        case CursorMovement::EOL:
        case CursorMovement::FORWARD_UP_TO_CHAR:
        case CursorMovement::FORWARD_ON_TO_CHAR:
        case CursorMovement::BACK_UP_TO_CHAR:
        case CursorMovement::BACK_ON_TO_CHAR:
            determine_new_cursor_screen_row();
            break;
        case CursorMovement::FIRST_LINE:
            m_cursor_screen_row = 0;
            break;
        case CursorMovement::LAST_LINE:
            m_cursor_screen_row = m_height - 1;
            break;
        }
    }

    return moved;
}

void BufferView::scroll_view_up(int n_lines, bool allow_eol)
{
    int orig_cursor_screen_row = m_cursor_screen_row;
    int so = effective_scroll_offset();
    int rows_to_move_cursor = (so + n_lines) - (m_height - m_cursor_screen_row - 1);
    rows_to_move_cursor = std::min(calculate_rows_above_screen(rows_to_move_cursor), rows_to_move_cursor);
    int actual_lines_moved = 0;
    while (rows_to_move_cursor-- > 0)
    {
        if (move_cursor_screen_row_up(allow_eol))
        {
            actual_lines_moved++;
            update();
        }
        else
        {
            break;
        }
    }
    m_cursor_screen_row = orig_cursor_screen_row + (n_lines - actual_lines_moved);
}

void BufferView::scroll_view_down(int n_lines, bool allow_eol)
{
    int orig_cursor_screen_row = m_cursor_screen_row;
    int so = effective_scroll_offset();
    int rows_to_move_cursor = (so + n_lines) - m_cursor_screen_row;
    rows_to_move_cursor = std::min(calculate_rows_below_screen(rows_to_move_cursor), rows_to_move_cursor);
    int actual_lines_moved = 0;
    while (rows_to_move_cursor-- > 0)
    {
        if (move_cursor_screen_row_down(allow_eol))
        {
            actual_lines_moved++;
            update();
        }
        else
        {
            break;
        }
    }
    m_cursor_screen_row = orig_cursor_screen_row - (n_lines - actual_lines_moved);
}

/**************************************************************************
 * Internal functions
 *************************************************************************/

void BufferView::determine_new_cursor_screen_row()
{
    if (m_lines.size() > 0)
    {
        if (m_iterator->line() < m_lines.begin()->line->line())
        {
            m_cursor_screen_row = 0;
            return;
        }
        if (m_iterator->line() > m_lines.rbegin()->line->line())
        {
            m_cursor_screen_row = m_height - 1;
            return;
        }
        for (const LineDescriptor & screen_line : m_lines)
        {
            if (screen_line.line->line() == m_iterator->line())
            {
                m_cursor_screen_row = screen_line.row_offset + m_cursor_row_offset;
                return;
            }
        }
    }
}

int BufferView::calculate_rows_in_line(std::shared_ptr<Buffer::Iterator> start_of_line)
{
    int saved_row_offset = 0;
    for (auto it = horiz_iter(start_of_line); it.is_valid(); it++)
    {
        if (!it.is_eol())
        {
            saved_row_offset = it.row_offset();
        }
        else
        {
            break;
        }
    }
    return saved_row_offset + 1;
}

int BufferView::calculate_rows_in_cursor_line(std::shared_ptr<Buffer::Iterator> start_of_line)
{
    int saved_row_offset = 0;
    for (auto it = horiz_iter(start_of_line); it.is_valid(); it++)
    {
        if (*it.iterator() == *m_iterator)
        {
            m_cursor_virtual_column = it.virtual_column();
            m_cursor_screen_column = it.screen_column();
            if (m_update_target_column)
            {
                m_target_screen_column = m_cursor_screen_column;
                m_target_virtual_column = m_cursor_virtual_column;
            }
            m_cursor_row_offset = it.row_offset();
        }
        if (!it.is_eol())
        {
            saved_row_offset = it.row_offset();
        }
        else
        {
            break;
        }
    }
    m_update_target_column = false;
    return saved_row_offset + 1;
}

int BufferView::screen_rows_below_line(std::shared_ptr<Buffer::Iterator> line)
{
    auto i = std::make_shared<Buffer::Iterator>(*line);
    int rows = 0;
    while ((rows < m_height) && i->go_next_line())
    {
        rows += calculate_rows_in_line(i);
    }
    return rows;
}

int BufferView::screen_rows_above_line(
        std::shared_ptr<Buffer::Iterator> line,
        std::list<std::pair<int, std::shared_ptr<Buffer::Iterator>>> & backward_lines)
{
    auto i = std::make_shared<Buffer::Iterator>(*line);
    int rows = 0;
    while ((rows < m_height) && i->go_previous_line())
    {
        int rows_in_this_line = calculate_rows_in_line(i);
        rows += rows_in_this_line;
        backward_lines.push_back(std::pair<int, std::shared_ptr<Buffer::Iterator>>(rows_in_this_line, std::make_shared<Buffer::Iterator>(*i)));
    }
    return rows;
}

void BufferView::move_forward_to_column(int to_column, bool allow_eol)
{
    auto start_of_line = std::make_shared<Buffer::Iterator>(*m_iterator);
    start_of_line->go_start_of_line();
    for (auto it = horiz_iter(start_of_line); it.is_valid(); it++)
    {
        if (allow_eol || (!it.is_eol()))
        {
            if (it.virtual_column() <= to_column)
            {
                *m_iterator = *it.iterator();
            }
            else
            {
                break;
            }
        }
    }
}

bool BufferView::move_cursor_screen_row_up(bool allow_eol)
{
    if (m_cursor_row_offset > 0)
    {
        /* There are screen rows above the current in the current line. */
        auto start_of_line = std::make_shared<Buffer::Iterator>(*m_iterator);
        start_of_line->go_start_of_line();
        move_forward_to_screen_position(start_of_line, m_cursor_row_offset - 1, allow_eol);
    }
    else
    {
        auto previous_line = std::make_shared<Buffer::Iterator>(*m_iterator);
        if (!previous_line->go_previous_line())
        {
            return false;
        }
        int target_row_offset = calculate_rows_in_line(previous_line) - 1;
        move_forward_to_screen_position(previous_line, target_row_offset, allow_eol);
    }
    return true;
}

bool BufferView::move_cursor_screen_row_down(bool allow_eol)
{
    if (m_rows_in_cursor_line > (m_cursor_row_offset + 1))
    {
        /* There are screen rows below the current in the current line. */
        auto start_of_line = std::make_shared<Buffer::Iterator>(*m_iterator);
        start_of_line->go_start_of_line();
        move_forward_to_screen_position(start_of_line, m_cursor_row_offset + 1, allow_eol);
    }
    else
    {
        auto next_line = std::make_shared<Buffer::Iterator>(*m_iterator);
        if (!next_line->go_next_line())
        {
            return false;
        }
        move_forward_to_screen_position(next_line, 0, allow_eol);
    }
    return true;
}

void BufferView::move_forward_to_screen_position(
        std::shared_ptr<Buffer::Iterator> line, int target_row_offset,
        bool allow_eol)
{
    *m_iterator = *line;
    bool moved_to_target_row = false;
    for (auto it = horiz_iter(line); it.is_valid(); it++)
    {
        if (((!it.is_eol()) || allow_eol) &&
                    ((it.row_offset() < target_row_offset) ||
                     (!moved_to_target_row) ||
                     (it.screen_column() <= m_target_screen_column)))
        {
            *m_iterator = *it.iterator();
            m_cursor_row_offset = it.row_offset();
            if (it.row_offset() == target_row_offset)
            {
                moved_to_target_row = true;
            }
        }
        else
        {
            break;
        }
    }
}

bool BufferView::move_cursor_forward_up_to_char(uint32_t c)
{
    Buffer::Iterator last_it = *m_iterator;
    Buffer::Iterator it = *m_iterator;
    while (it.go_right_in_line(false))
    {
        if (*it == c)
        {
            if (last_it != *m_iterator)
            {
                *m_iterator = last_it;
                return true;
            }
            else
            {
                return false;
            }
        }
        last_it = it;
    }
    return false;
}

bool BufferView::move_cursor_forward_on_to_char(uint32_t c)
{
    Buffer::Iterator it = *m_iterator;
    while (it.go_right_in_line(false))
    {
        if (*it == c)
        {
            *m_iterator = it;
            return true;
        }
    }
    return false;
}

bool BufferView::move_cursor_backward_up_to_char(uint32_t c)
{
    Buffer::Iterator last_it = *m_iterator;
    Buffer::Iterator it = *m_iterator;
    while (it.go_left_in_line())
    {
        if (*it == c)
        {
            if (last_it != *m_iterator)
            {
                *m_iterator = last_it;
                return true;
            }
            else
            {
                return false;
            }
        }
        last_it = it;
    }
    return false;
}

bool BufferView::move_cursor_backward_on_to_char(uint32_t c)
{
    Buffer::Iterator it = *m_iterator;
    while (it.go_left_in_line())
    {
        if (*it == c)
        {
            *m_iterator = it;
            return true;
        }
    }
    return false;
}

int BufferView::calculate_rows_above_screen(int stop_at)
{
    if (m_lines.empty())
        return 0;
    const LineDescriptor & first_line = *m_lines.begin();
    auto iterator = std::make_shared<Buffer::Iterator>(*first_line.line);
    int lines = std::max(0, -first_line.row_offset);
    while (lines < stop_at)
    {
        if (!iterator->go_previous_line())
            break;
        lines += calculate_rows_in_line(iterator);
    }
    return lines;
}

int BufferView::calculate_rows_below_screen(int stop_at)
{
    if (m_lines.empty())
        return 0;
    const LineDescriptor & last_line = *m_lines.rbegin();
    auto iterator = std::make_shared<Buffer::Iterator>(*last_line.line);
    int lines = std::max(0, last_line.row_offset + last_line.n_rows - m_height);
    while (lines < stop_at)
    {
        if (!iterator->go_next_line())
            break;
        lines += calculate_rows_in_line(iterator);
    }
    return lines;
}
