#include "BufferPane.h"

BufferPane::BufferPane(Window * window, std::shared_ptr<Buffer> buffer)
    : m_window(window), m_buffer(buffer)
{
    m_cursor_screen_row = 0;
    m_scroll_offset = 5;
    m_iterator = buffer->add_cursor();
    m_target_column = 0;
    m_cursor_virtual_column = 0;
}

void BufferPane::resize(int width, int height)
{
    Pane::resize(width, height);
    m_columns = std::max(1, m_width / m_window->font()->get_advance());
    m_rows = std::max(1, (m_height - m_window->font()->get_line_height() - 1) / m_window->font()->get_line_height());
}

void BufferPane::walk_line(const Buffer::Iterator & start_of_line, std::function<void(int, int, int, int, const Buffer::Iterator &)> callback)
{
    int row_offset = 0;
    int screen_column = 0;
    int virtual_column = 0;
    Buffer::Iterator i = start_of_line;
    for (;;)
    {
        uint32_t code_point = *i;
        if ((code_point == '\n') || (!i.valid()))
        {
            callback(row_offset, screen_column, virtual_column, 0, i);
            break;
        }
        int c_width;
        if (code_point == '\t')
        {
            uint8_t tabstop = m_buffer->tabstop();
            c_width = tabstop - virtual_column % tabstop;
        }
        else
        {
            c_width = character_width(code_point);
            if (((screen_column + c_width) > m_columns) &&
                    (screen_column > 0))
            {
                row_offset++;
                screen_column = 0;
            }
        }
        callback(row_offset, screen_column, virtual_column, c_width, i);
        virtual_column += c_width;
        if (code_point == '\t')
        {
            screen_column += c_width;
            while (screen_column >= m_columns)
            {
                screen_column -= m_columns;
                row_offset++;
            }
        }
        else
        {
            if ((screen_column + c_width) >= m_columns)
            {
                row_offset++;
                screen_column = 0;
            }
            else
            {
                screen_column += c_width;
            }
        }
        i.go_forward();
    }
}

int BufferPane::calculate_rows_in_line(const Buffer::Iterator & start_of_line)
{
    int saved_row_offset = 0;
    walk_line(start_of_line, [&saved_row_offset](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        uint32_t code_point = *i;
        if ((code_point != '\n') && (code_point != Buffer::Iterator::INVALID_CODE_POINT))
        {
            saved_row_offset = row_offset;
        }
    });
    return saved_row_offset + 1;
}

int BufferPane::calculate_rows_in_cursor_line(const Buffer::Iterator & start_of_line)
{
    int saved_row_offset = 0;
    walk_line(start_of_line, [this, &saved_row_offset](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        uint32_t code_point = *i;
        if (i == *m_iterator)
        {
            m_cursor_virtual_column = virtual_column;
            m_cursor_screen_column = screen_column;
            m_cursor_row_offset = row_offset;
        }
        if ((code_point != '\n') && (code_point != Buffer::Iterator::INVALID_CODE_POINT))
        {
            saved_row_offset = row_offset;
        }
    });
    return saved_row_offset + 1;
}

int BufferPane::calculate_rows_in_line_with_iterator_offset(const Buffer::Iterator & start_of_line, const Buffer::Iterator & reference, int * iterator_row_offset)
{
    int saved_row_offset = 0;
    walk_line(start_of_line, [this, &saved_row_offset, &reference, &iterator_row_offset](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        uint32_t code_point = *i;
        if (i == reference)
        {
            *iterator_row_offset = row_offset;
        }
        if ((code_point != '\n') && (code_point != Buffer::Iterator::INVALID_CODE_POINT))
        {
            saved_row_offset = row_offset;
        }
    });
    return saved_row_offset + 1;
}

int BufferPane::screen_rows_below_line(const Buffer::Iterator & line)
{
    Buffer::Iterator i = line;
    int rows = 0;
    while ((rows < m_rows) && i.go_next_line())
    {
        rows += calculate_rows_in_line(i);
    }
    return rows;
}

int BufferPane::screen_rows_above_line(const Buffer::Iterator & line, std::list<std::pair<int, Buffer::Iterator>> & backward_lines)
{
    Buffer::Iterator i = line;
    int rows = 0;
    while ((rows < m_rows) && 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, Buffer::Iterator>(rows_in_this_line, i));
    }
    return rows;
}

void BufferPane::update_cursor_row(std::list<std::pair<int, Buffer::Iterator>> & backward_lines)
{
    Buffer::Iterator start_of_line = *m_iterator;
    start_of_line.go_start_of_line();
    int rows_in_cursor_line = calculate_rows_in_cursor_line(start_of_line);
    int so = effective_scroll_offset();
    int rows_above = screen_rows_above_line(*m_iterator, backward_lines) + m_cursor_row_offset;
    int rows_below = screen_rows_below_line(*m_iterator) + std::max(0, 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_rows - 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_rows - rows_below - 1, m_cursor_screen_row);
    m_cursor_screen_row = std::min(rows_above, m_cursor_screen_row);
}

bool BufferPane::move_cursor_screen_row_up()
{
    size_t previous_iterator_offset = m_iterator->offset();
    if (m_cursor_row_offset > 0)
    {
        Buffer::Iterator start_of_line = *m_iterator;
        start_of_line.go_start_of_line();
        walk_line(start_of_line, [this](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
            if (((*i != '\n') || insert_mode()) &&
                (screen_column <= m_cursor_screen_column) &&
                (row_offset < m_cursor_row_offset))
            {
                *m_iterator = i;
            }
        });
    }
    else
    {
        if (!m_iterator->go_previous_line())
        {
            return false;
        }
        walk_line(*m_iterator, [this](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
            if (((*i != '\n') || insert_mode()) &&
                (screen_column <= m_cursor_screen_column))
            {
                *m_iterator = i;
            }
        });
    }
    return m_iterator->offset() != previous_iterator_offset;
}

bool BufferPane::move_cursor_screen_row_down()
{
    size_t previous_iterator_offset = m_iterator->offset();
    Buffer::Iterator start_of_line = *m_iterator;
    start_of_line.go_start_of_line();
    walk_line(start_of_line, [this](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        if (((*i != '\n') || insert_mode()) &&
            (screen_column <= m_cursor_screen_column) &&
            (row_offset > m_cursor_row_offset))
        {
            *m_iterator = i;
        }
    });
    if (m_iterator->offset() != previous_iterator_offset)
    {
        return true;
    }
    if (!m_iterator->go_next_line())
    {
        return false;
    }
    walk_line(*m_iterator, [this](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        if (((*i != '\n') || insert_mode()) &&
            (screen_column <= m_cursor_screen_column) &&
            (row_offset == 0))
        {
            *m_iterator = i;
        }
    });
    return m_iterator->offset() != previous_iterator_offset;
}

int BufferPane::determine_new_cursor_screen_row()
{
    if (m_screen_lines.size() > 0)
    {
        if (m_iterator->line() < m_screen_lines.begin()->second.line())
        {
            return 0;
        }
        if (m_iterator->line() > m_screen_lines.rbegin()->second.line())
        {
            return m_rows;
        }
        for (auto screen_line : m_screen_lines)
        {
            if (screen_line.second.line() == m_iterator->line())
            {
                calculate_rows_in_cursor_line(screen_line.second);
                return screen_line.first + m_cursor_row_offset;
            }
        }
    }
    return 0;
}

void BufferPane::draw()
{
    m_screen_lines.clear();
    if (m_iterator->valid())
    {
        std::list<std::pair<int, Buffer::Iterator>> backward_lines;
        update_cursor_row(backward_lines);
        int screen_row = m_cursor_screen_row - m_cursor_row_offset;
        Buffer::Iterator i = *m_iterator;
        if (screen_row <= 0)
        {
            i.go_start_of_line();
        }
        else for (auto rows_iterator_pair : backward_lines)
        {
            screen_row -= rows_iterator_pair.first;
            i = rows_iterator_pair.second;
            if (screen_row <= 0)
            {
                break;
            }
        }
        while (screen_row <= m_rows)
        {
            m_screen_lines.push_back(std::pair<int, Buffer::Iterator>(screen_row, i));
            screen_row += draw_buffer_line(screen_row, i);
            if (!i.go_next_line())
            {
                break;
            }
        }
    }
    else
    {
        draw_cursor(col_x(0), row_y(0));
    }
    draw_status_bar();
}

int BufferPane::draw_buffer_line(int screen_row, const Buffer::Iterator & start_of_line)
{
    int saved_row_offset = 0;
    walk_line(start_of_line, [this, &saved_row_offset, &screen_row](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        uint32_t code_point = *i;
        int draw_row = screen_row + row_offset;
        if ((draw_row >= 0) && (draw_row <= m_rows))
        {
            if (i == *m_iterator)
            {
                bool wrap = (code_point == '\t');
                int row = draw_row;
                int col = screen_column;
                int w = insert_mode() ? 1 : std::max(1, character_width);
                while (w--)
                {
                    draw_cursor(col_x(col), row_y(row));
                    col++;
                    if (wrap && (col >= m_columns))
                    {
                        row++;
                        col = 0;
                    }
                }
            }
            draw_character(code_point, draw_row, screen_column);
            if ((code_point != '\n') && (code_point != Buffer::Iterator::INVALID_CODE_POINT))
            {
                saved_row_offset = row_offset;
            }
        }
    });
    return saved_row_offset + 1;
}

void BufferPane::draw_character(uint32_t character, int screen_row, int screen_column)
{
    if ((character != ' ') &&
        (character != '\t') &&
        (character != '\n') &&
        (character != Buffer::Iterator::INVALID_CODE_POINT))
    {
        int x = col_x(screen_column);
        int y = row_y(screen_row);
        if (character < 0x20u)
        {
            m_window->gl()->draw_rect(win_x(x + 2), win_y(y),
                    m_window->font()->get_advance() * 2 - 4, 1,
                    0, 0.7, 1.0, 1.0);
            m_window->gl()->draw_character(win_x(x), win_y(y), '^', *m_window->font(), 1.0, 1.0, 1.0, 1.0);
            m_window->gl()->draw_character(win_x(col_x(screen_column + 1)), win_y(y), character | 0x40u, *m_window->font(), 1.0, 1.0, 1.0, 1.0);
        }
        else
        {
            m_window->gl()->draw_character(win_x(x), win_y(y), character, *m_window->font(), 1.0, 1.0, 1.0, 1.0);
        }
    }
}

int BufferPane::character_width(uint32_t character)
{
    if (character < 0x20u)
    {
        return 2;
    }
    else if (character < 0x80u)
    {
        return 1u;
    }
    else
    {
        return std::max(1, m_window->font()->get_glyph(character)->get_advance() / m_window->font()->get_advance());
    }
}

void BufferPane::draw_buffer_character(int screen_column, int screen_row, uint32_t character)
{
    if (character < 0x20u)
    {
        m_window->gl()->draw_character(win_x(col_x(screen_column)), win_y(row_y(screen_row)),
                '^', *m_window->font(), 1.0, 1.0, 1.0, 1.0);
        m_window->gl()->draw_character(win_x(col_x(screen_column + 1)), win_y(row_y(screen_row)),
                (character - 0x20u + 'A'), *m_window->font(), 1.0, 1.0, 1.0, 1.0);
    }
    else
    {
        m_window->gl()->draw_character(win_x(col_x(screen_column)), win_y(row_y(screen_row)),
                character, *m_window->font(), 1.0, 1.0, 1.0, 1.0);
    }
}

void BufferPane::draw_cursor(int x, int y)
{
    int width = m_window->font()->get_advance();
    int height = m_window->font()->get_line_height();
    if (insert_mode())
    {
        m_window->gl()->draw_rect(win_x(x), win_y(y), 1, height, 1.0, 0.2, 1.0, 1.0);
        m_window->gl()->draw_rect(win_x(x + 1), win_y(y + height - 1), 2, 1, 1.0, 0.2, 1.0, 1.0);
        m_window->gl()->draw_rect(win_x(x + 1), win_y(y), 2, 1, 1.0, 0.2, 1.0, 1.0);
    }
    else
    {
        m_window->gl()->draw_rect(win_x(x), win_y(y), width, height, 1.0, 0.2, 1.0, 1.0);
    }
}

void BufferPane::exit_insert_mode()
{
    if (insert_mode())
    {
        m_buffer->exit_insert_mode();
        if (!m_iterator->is_start_of_line())
        {
            m_iterator->go_back();
        }
        m_window->request_redraw();
    }
}

void BufferPane::insert_code_point(uint32_t code_point)
{
    if (insert_mode())
    {
        if (code_point == '\b')
        {
            Buffer::Iterator i = *m_iterator;
            i.go_back();
            if (i.valid())
            {
                m_buffer->erase_code_point(i);
            }
        }
        else
        {
            if (code_point == '\r')
            {
                code_point = '\n';
                m_cursor_screen_row++;
            }
            m_buffer->insert_code_point(*m_iterator, code_point);
        }
        m_window->request_redraw();
    }
}

void BufferPane::kill_character_at_cursor()
{
    if (**m_iterator != '\n')
    {
        m_buffer->erase_code_point(*m_iterator);
        if (**m_iterator == '\n')
        {
            m_iterator->go_left_in_line();
        }
        m_window->request_redraw();
    }
}

void BufferPane::write_file()
{
    if (m_buffer->filename() != "")
    {
        m_buffer->write_to_file(m_buffer->filename().c_str());
    }
}

size_t BufferPane::display_column() const
{
    if (!m_iterator->valid())
    {
        return 0u;
    }
    if ((**m_iterator == '\n') && (!insert_mode()))
    {
        Buffer::Iterator i = *m_iterator;
        if (!i.go_left_in_line())
        {
            return 0u;
        }
    }
    return m_cursor_virtual_column + 1u;
}

void BufferPane::cursor_move(Window::CursorMovement which)
{
    bool moved = false;

    switch (which)
    {
    case Window::CursorMovement::LEFT:
        moved = m_iterator->go_left_in_line();
        break;
    case Window::CursorMovement::RIGHT:
        moved = m_iterator->go_right_in_line(insert_mode());
        break;
    case Window::CursorMovement::UP:
        moved = m_iterator->go_previous_line();
        break;
    case Window::CursorMovement::DOWN:
        moved = m_iterator->go_next_line();
        break;
    case Window::CursorMovement::SOL:
        moved = m_iterator->go_start_of_line();
        break;
    case Window::CursorMovement::EOL:
        moved = m_iterator->go_end_of_line(insert_mode());
        break;
    case Window::CursorMovement::FIRST_LINE:
        {
            auto it = m_buffer->begin();
            if (it != *m_iterator)
            {
                *m_iterator = it;
                moved = true;
            }
        }
        break;
    case Window::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;
            }
        }
        break;
    case Window::CursorMovement::SCREEN_ROW_UP:
        moved = move_cursor_screen_row_up();
        break;
    case Window::CursorMovement::SCREEN_ROW_DOWN:
        moved = move_cursor_screen_row_down();
        break;
    }
    if (moved)
    {
        switch (which)
        {
        case Window::CursorMovement::LEFT:
        case Window::CursorMovement::RIGHT:
            {
                Buffer::Iterator start_of_line = *m_iterator;
                start_of_line.go_start_of_line();
                calculate_rows_in_cursor_line(start_of_line);
                m_target_column = m_cursor_virtual_column;
            }
            break;
        case Window::CursorMovement::UP:
            forward_to_column(m_target_column, insert_mode());
            break;
        case Window::CursorMovement::DOWN:
            forward_to_column(m_target_column, insert_mode());
            break;
        case Window::CursorMovement::SOL:
            m_target_column = 0;
            break;
        case Window::CursorMovement::EOL:
            m_target_column = INT_MAX;
            break;
        case Window::CursorMovement::FIRST_LINE:
            m_cursor_screen_row = 0;
            break;
        case Window::CursorMovement::LAST_LINE:
            m_cursor_screen_row = m_rows;
            break;
        }
        m_cursor_screen_row = determine_new_cursor_screen_row();
        m_window->request_redraw();
    }
}

void BufferPane::enter_insert_mode(Window::EnterInsertModeMode which)
{
    if (insert_mode())
        return;

    switch (which)
    {
    case Window::EnterInsertModeMode::START_OF_CHAR:
        m_buffer->enter_insert_mode();
        break;
    case Window::EnterInsertModeMode::END_OF_CHAR:
        if (**m_iterator != '\n')
        {
            m_iterator->go_forward();
        }
        m_buffer->enter_insert_mode();
        break;
    case Window::EnterInsertModeMode::START_OF_LINE:
        m_iterator->go_start_of_line();
        enter_insert_mode(Window::EnterInsertModeMode::START_OF_CHAR);
        break;
    case Window::EnterInsertModeMode::END_OF_LINE:
        m_iterator->go_end_of_line(false);
        enter_insert_mode(Window::EnterInsertModeMode::END_OF_CHAR);
        break;
    case Window::EnterInsertModeMode::NEW_LINE_BEFORE:
        m_iterator->go_start_of_line();
        m_buffer->enter_insert_mode();
        m_buffer->insert_code_point(*m_iterator, '\n');
        m_buffer->exit_insert_mode();
        cursor_move(Window::CursorMovement::UP);
        enter_insert_mode(Window::EnterInsertModeMode::START_OF_CHAR);
        break;
    case Window::EnterInsertModeMode::NEW_LINE_AFTER:
        m_iterator->go_end_of_line(true);
        m_buffer->enter_insert_mode();
        m_buffer->insert_code_point(*m_iterator, '\n');
        break;
    }

    m_window->request_redraw();
}

int BufferPane::calculate_lines_to_scroll(Window::ScrollMode scroll_mode)
{
    switch (scroll_mode)
    {
    case Window::ScrollMode::ONE_LINE:
        return 1;
        break;
    case Window::ScrollMode::WHEEL:
        return 3;
        break;
    case Window::ScrollMode::HALF_SCREEN:
        return m_rows / 2;
        break;
    case Window::ScrollMode::WHOLE_SCREEN:
        return m_rows - 1;
        break;
    }
    return 0;
}

void BufferPane::scroll_window_up(Window::ScrollMode scroll_mode)
{
    int lines_to_scroll = calculate_lines_to_scroll(scroll_mode);
    int so = effective_scroll_offset();
    int lines_to_move_cursor = (so + lines_to_scroll) - (m_rows - m_cursor_screen_row - 1);
    while (lines_to_move_cursor-- > 0)
    {
        move_cursor_screen_row_up();
    }
    m_cursor_screen_row += lines_to_scroll;
    m_window->request_redraw();
}

void BufferPane::scroll_window_down(Window::ScrollMode scroll_mode)
{
    int lines_to_scroll = calculate_lines_to_scroll(scroll_mode);
    int so = effective_scroll_offset();
    int lines_to_move_cursor = (so + lines_to_scroll) - m_cursor_screen_row;
    while (lines_to_move_cursor-- > 0)
    {
        move_cursor_screen_row_down();
    }
    m_cursor_screen_row -= lines_to_scroll;
    m_window->request_redraw();
}

void BufferPane::forward_to_column(int column, bool allow_eol)
{
    Buffer::Iterator start_of_line = *m_iterator;
    start_of_line.go_start_of_line();
    walk_line(start_of_line, [this, &column, &allow_eol](int row_offset, int screen_column, int virtual_column, int character_width, const Buffer::Iterator & i) {
        uint32_t code_point = *i;
        if ((code_point != '\n') || allow_eol)
        {
            if (virtual_column <= column)
            {
                *m_iterator = i;
            }
        }
    });
}

void BufferPane::draw_status_bar()
{
    m_window->gl()->draw_rect(win_x(0), win_y(m_window->font()->get_line_height()), m_width, 1, 0.5, 0.5, 0.5, 1.0);
    m_window->gl()->draw_rect(win_x(0), win_y(0), m_width, m_window->font()->get_line_height(), 0.0, 0.0, 0.0, 1.0);
    char cursor_position[20];
    sprintf(cursor_position, "%zu, %zu", display_line(), display_column());
    int cursor_position_length = strlen(cursor_position);
    int x = m_width - m_window->font()->get_advance() * cursor_position_length;
    m_window->gl()->draw_rect(win_x(x - 2), win_y(0), 1, m_window->font()->get_line_height(), 0.5, 0.5, 0.5, 1.0);
    std::string filename = m_buffer->filename();
    if (filename == "")
    {
        filename = "[No Name]";
    }
    int filename_x = std::min(0, x - 3 - (int)filename.size() * m_window->font()->get_advance());
    for (int i = 0; i < cursor_position_length; i++)
    {
        m_window->gl()->draw_character(win_x(x), win_y(0), cursor_position[i], *m_window->font(), 1.0, 1.0, 1.0, 1.0);
        x += m_window->font()->get_advance();
    }
    for (size_t i = 0; i < filename.size(); i++)
    {
        m_window->gl()->draw_character(win_x(filename_x), win_y(0), filename[i], *m_window->font(), 1.0, 1.0, 1.0, 1.0);
        filename_x += m_window->font()->get_advance();
    }
}

void BufferPane::undo()
{
    m_buffer->undo();
    m_window->request_redraw();
}

void BufferPane::redo()
{
    m_buffer->redo();
    m_window->request_redraw();
}
