/*
 * Copyright (C) 2023 Kyle Reed
 *
 * This file is part of PortaPack.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street,
 * Boston, MA 02110-1301, USA.
 */

#include "ui_fileman.hpp"
#include "ui_text_editor.hpp"
#include "ui_textentry.hpp"

#include "log_file.hpp"
#include "string_format.hpp"

#include "portapack_persistent_memory.hpp"

using namespace portapack;
namespace fs = std::filesystem;

namespace ui {

/* TextViewer *******************************************************/

TextViewer::TextViewer(Rect parent_rect)
    : Widget(parent_rect) {
    set_focusable(true);
    set_font_zoom(false);
}

void TextViewer::paint(Painter& painter) {
    auto first_line = paint_state_.first_line;
    auto first_col = paint_state_.first_col;

    if (!has_file())
        return;

    // Move the viewport vertically.
    if (cursor_.line < first_line)
        first_line = cursor_.line;
    else if (cursor_.line >= first_line + max_line)
        first_line = cursor_.line - max_line + 1;

    // Move the viewport horizontally.
    if (cursor_.col < first_col)
        first_col = cursor_.col;
    if (cursor_.col >= first_col + max_col)
        first_col = cursor_.col - max_col + 1;

    // Viewport updated? Redraw text.
    if (first_line != paint_state_.first_line ||
        first_col != paint_state_.first_col) {
        paint_state_.first_line = first_line;
        paint_state_.first_col = first_col;
        paint_state_.redraw_text = true;
        paint_state_.line = UINT32_MAX;  // forget old cursor position when overwritten
    }

    if (paint_state_.redraw_text) {
        paint_text(painter, first_line, first_col);
        paint_state_.redraw_text = false;
    }

    if (paint_state_.redraw_marked) {
        paint_marked(painter);
        paint_state_.redraw_marked = false;
    }

    paint_cursor(painter);
}

bool TextViewer::on_key(const KeyEvent key) {
    int16_t delta_col = 0;
    int16_t delta_line = 0;

    if (key == KeyEvent::Left)
        delta_col = -1;
    else if (key == KeyEvent::Right)
        delta_col = 1;
    else if (key == KeyEvent::Up)
        delta_line = -1;
    else if (key == KeyEvent::Down)
        delta_line = 1;
    else if (key == KeyEvent::Select && on_select) {
        on_select();
        return true;
    }

    // Always allow cursor direction to be updated.
    cursor_.dir = delta_col != 0 ? ScrollDirection::Horizontal : ScrollDirection::Vertical;
    auto updated = apply_scrolling_constraints(delta_line, delta_col);

    if (updated)
        redraw();

    return updated;
}

bool TextViewer::on_encoder(EncoderEvent delta) {
    bool updated = false;

    if (cursor_.dir == ScrollDirection::Horizontal)
        updated = apply_scrolling_constraints(0, delta);
    else {
        delta *= 16;
        updated = apply_scrolling_constraints(delta, 0);
    }

    if (updated)
        redraw();

    return updated;
}

void TextViewer::redraw(bool redraw_text, bool redraw_marked) {
    paint_state_.redraw_text = redraw_text;
    paint_state_.redraw_marked = redraw_marked;
    set_dirty();
}

uint32_t TextViewer::offset() const {
    auto range = file_->line_range(cursor_.line);
    if (range)
        return range->start + col();
    return 0;
}

void TextViewer::cursor_home() {
    cursor_.col = 0;
    redraw();
}

void TextViewer::cursor_end() {
    cursor_.col = line_length() - 1;
    redraw();
}

void TextViewer::cursor_set(uint16_t line, uint16_t col) {
    cursor_.line = line;
    cursor_.col = col;
}

void TextViewer::cursor_mark_selected() {
    LineColPair newMarker = std::make_pair(cursor_.line, cursor_.col);
    auto it = std::find(lineColPair.begin(), lineColPair.end(), newMarker);

    if (it != lineColPair.end()) {
        lineColPair.erase(it);
    } else {
        lineColPair.push_back(newMarker);
    }

    // Mark pending change.
    cursor_.mark_change = false;

    redraw();
}

void TextViewer::cursor_clear_marked() {
    lineColPair.clear();
    redraw(true, true);
}

uint16_t TextViewer::line_length() {
    return file_->line_length(cursor_.line);
}

bool TextViewer::apply_scrolling_constraints(int16_t delta_line, int16_t delta_col) {
    if (!has_file())
        return false;

    int32_t new_line = cursor_.line + delta_line;
    int32_t new_col = cursor_.col + delta_col;
    int32_t new_line_length = file_->line_length(new_line);

    if (new_col < 0)
        --new_line;
    else if (new_col >= new_line_length && delta_line == 0) {
        // Only wrap if moving horizontally.
        new_col = 0;
        ++new_line;
    }

    // Snap to first/last line to make navigating easier.
    if (new_line < 0 && cursor_.line > 0) {
        new_line = 0;
    } else if (new_line >= (int32_t)file_->line_count()) {
        auto last_line = file_->line_count() - 1;

        if (cursor_.line < last_line)
            new_line = last_line;
    }

    if (new_line < 0 || (uint32_t)new_line >= file_->line_count())
        return false;

    new_line_length = file_->line_length(new_line);

    // Wrap or clamp column.
    if (new_line_length == 0)
        new_col = 0;
    else if (new_col >= new_line_length || new_col < 0)
        new_col = new_line_length - 1;

    cursor_.line = new_line;
    cursor_.col = new_col;

    if (on_cursor_moved)
        on_cursor_moved();

    return true;
}

void TextViewer::paint_text(Painter& painter, uint32_t line, uint16_t col) {
    auto r = screen_rect();
    char buffer[max_col + 1];

    // Draw the lines from the file
    for (auto i = 0u; i < max_line; ++i) {
        if (line + i >= file_->line_count())
            break;

        auto result = file_->get_text(line + i, col, buffer, max_col);

        if (result && *result > 0)
            painter.draw_string(
                {0, r.top() + (int)i * char_height},
                style(), {buffer, *result});

        // Clear empty line sections. This is less visually jarring than full clear.
        int32_t clear_width = max_col - (result ? *result : 0);
        if (clear_width > 0)
            painter.fill_rectangle(
                {(max_col - clear_width) * char_width,
                 r.top() + (int)i * char_height,
                 clear_width * char_width, char_height},
                style().background);

        // if cursor line got overwritten, disable XOR of old cursor when displaying new cursor
        if (i == paint_state_.line)
            paint_state_.line = UINT32_MAX;
    }
}

void TextViewer::paint_cursor(Painter& painter) {
    if (!has_focus())
        return;

    auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
        int cursor_width = char_width + 1;
        int x = (col - paint_state_.first_col) * char_width - 1;
        if (x < 0) {  // cursor is one pixel narrower when in left column
            cursor_width--;
            x = 0;
        }
        int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;

        // Converting one row at a time to reduce buffer size
        auto pbuf8 = cursor_.pixel_buffer8;
        auto pbuf = cursor_.pixel_buffer;
        for (auto col = 0; col < char_height; col++) {
            // Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
            portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
            for (auto i = 0; i < cursor_width; i++)
                pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
            portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
        }
    };

    if (paint_state_.line != UINT32_MAX)  // only XOR old cursor if it still appears on the screen
    {
        // Only reset previous cursor if we aren't marking.
        if (paint_state_.mark_change) {
            xor_cursor(paint_state_.line, paint_state_.col);
        }
    }

    xor_cursor(cursor_.line, cursor_.col);

    paint_state_.line = cursor_.line;
    paint_state_.col = cursor_.col;
    paint_state_.mark_change = cursor_.mark_change;

    // Reset marking and wait for new change.
    cursor_.mark_change = true;
}

void TextViewer::paint_marked(Painter& painter) {
    auto xor_cursor = [this, &painter](int32_t line, uint16_t col) {
        int cursor_width = char_width + 1;
        int x = (col - paint_state_.first_col) * char_width - 1;
        if (x < 0) {  // cursor is one pixel narrower when in left column
            cursor_width--;
            x = 0;
        }
        int y = screen_rect().top() + (line - paint_state_.first_line) * char_height;

        // Converting one row at a time to reduce buffer size
        auto pbuf8 = cursor_.pixel_buffer8;
        auto pbuf = cursor_.pixel_buffer;
        for (auto col = 0; col < char_height; col++) {
            // Annoyingly, read_pixels uses a 24-bit pixel format vs draw_pixels which uses 16-bit
            portapack::display.read_pixels({x, y + col, cursor_width, 1}, pbuf8, cursor_width);
            for (auto i = 0; i < cursor_width; i++)
                pbuf[i] = Color(pbuf8[i].r, pbuf8[i].g, pbuf8[i].b).v ^ 0xFFFF;
            portapack::display.draw_pixels({x, y + col, cursor_width, 1}, pbuf, cursor_width);
        }
    };

    auto it = lineColPair.begin();

    while (it != lineColPair.end()) {
        LineColPair entry = (LineColPair)*it;
        xor_cursor(entry.first, entry.second);
        it++;
    }
}

void TextViewer::reset_file(FileWrapper* file) {
    file_ = file;
    paint_state_.first_line = 0;
    paint_state_.first_col = 0;
    cursor_.line = 0;
    cursor_.col = 0;
    redraw(true);
}

void TextViewer::set_font_zoom(bool zoom) {
    font_zoom = zoom;
    font_style = font_zoom ? &Styles::white : &Styles::white_small;
    char_height = style().font.line_height();
    char_width = style().font.char_width();
    max_line = (uint8_t)(parent_rect().height() / char_height);
    max_col = (uint8_t)(parent_rect().width() / char_width);
}

/* TextEditorMenu ***************************************************/

TextEditorMenu::TextEditorMenu()
    : View{{7 * 4, 9 * 4, 25 * 8, 25 * 8}} {
    add_children(
        {
            &rect_frame,
            &button_home,
            &button_end,
            &button_zoom,
            &button_delline,
            &button_edit,
            &button_addline,
            &button_open,
            &button_save,
            &button_exit,
        });
}

void TextEditorMenu::on_show() {
    hide_children(false);
    button_edit.focus();
}

void TextEditorMenu::on_hide() {
    hide_children(true);
}

void TextEditorMenu::hide_children(bool hidden) {
    for (auto child : children()) {
        child->hidden(hidden);
    }
}

/* TextEditorView ***************************************************/

static fs::path get_temp_path(const fs::path& path) {
    if (!path.empty())
        return path + "~";

    return {};
}

static void delete_temp_file(const fs::path& path) {
    auto temp_path = get_temp_path(path);
    if (!temp_path.empty()) {
        delete_file(temp_path);
    }
}

static void save_temp_file(const fs::path& path) {
    delete_file(path);
    copy_file(get_temp_path(path), path);
}

static void show_save_prompt(
    NavigationView& nav,
    std::function<void()> on_save,
    std::function<void()> continuation) {
    nav.display_modal(
        "Save?", "       Save changes?", YESNO,
        [on_save](bool choice) {
            if (choice && on_save)
                on_save();
        });
    nav.set_on_pop(continuation);
}

TextEditorView::TextEditorView(NavigationView& nav)
    : nav_{nav} {
    add_children(
        {
            &viewer,
            &menu,
            &button_menu,
            &text_position,
            &text_size,
        });

    viewer.set_font_zoom(enable_zoom);

    viewer.on_select = [this]() {
        // Treat as if menu button was pressed.
        if (button_menu.on_select)
            button_menu.on_select();
    };

    viewer.on_cursor_moved = [this]() {
        update_position();
    };

    menu.hidden(true);
    menu.on_home() = [this]() {
        viewer.cursor_home();
        hide_menu(true);
    };

    menu.on_end() = [this]() {
        viewer.cursor_end();
        hide_menu(true);
    };

    menu.on_zoom() = [this]() {
        enable_zoom = viewer.toggle_font_zoom();
        refresh_ui();
        hide_menu(true);
    };

    menu.on_delete_line() = [this]() {
        prepare_for_write();
        file_->delete_line(viewer.line());
        refresh_ui();
        hide_menu(true);
    };

    menu.on_edit_line() = [this]() {
        show_edit_line();
    };

    menu.on_add_line() = [this]() {
        prepare_for_write();

        if (viewer.offset() < file_->size() - 1)
            file_->insert_line(viewer.line());
        else
            file_->insert_line(-1);  // Add after last line.

        refresh_ui();
        hide_menu(true);
    };

    menu.on_open() = [this]() {
        show_save_prompt([this]() {
            show_file_picker();
        });
    };

    menu.on_save() = [this]() {
        save_temp_file();
        hide_menu(true);
    };

    menu.on_exit() = [this]() {
        nav_.pop();
    };

    button_menu.on_select = [this]() {
        if (file_) {
            // Toggle menu.
            hide_menu(!menu.hidden());
        } else {
            show_file_picker();
        }
    };
}

TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
    : TextEditorView(nav) {
    open_file(path);
}

TextEditorView::~TextEditorView() {
    // NB: Be careful here. The UI will render after this instance
    // has been destroyed. Everything needed to render the UI
    // and perform the save actions must be value captured.
    if (file_dirty_) {
        ui::show_save_prompt(
            nav_,
            [p = path_]() { ui::save_temp_file(p); },
            [p = std::move(path_)]() { delete_temp_file(p); });
    }
}

void TextEditorView::on_show() {
    if (file_)
        viewer.focus();
    else
        button_menu.focus();
}

void TextEditorView::open_file(const fs::path& path) {
    file_.reset();
    viewer.clear_file();
    delete_temp_file(path_);

    path_ = {};
    file_dirty_ = false;
    has_temp_file_ = false;
    auto result = FileWrapper::open(
        path, false, [](uint32_t value, uint32_t total) {
            Painter p;
            auto percent = (value * 100) / total;
            auto width = (percent * screen_width) / 100;
            p.draw_hline({0, 16}, width, Color::yellow());
        });

    if (!result) {
        nav_.display_modal("Read Error", "Cannot open file:\n" + result.error().what());

    } else {
        file_ = *std::move(result);
        path_ = path;
        viewer.set_file(*file_);
    }

    portapack::persistent_memory::set_apply_fake_brightness(false);  // work around to resolve the display issue in notepad app. not elegant i know, so TODO.

    refresh_ui();
}

void TextEditorView::refresh_ui() {
    if (file_) {
        update_position();
        text_size.set(
            "Lines:" + to_string_dec_uint(file_->line_count()) +
            " (" + to_string_file_size(file_->size()) + ")");
    } else {
        text_position.set("");
        text_size.set("");
    }
}

void TextEditorView::update_position() {
    if (viewer.has_file()) {
        text_position.set(
            "Ln " + to_string_dec_uint(viewer.line() + 1) +
            ", Col " + to_string_dec_uint(viewer.col() + 1));
    }
}

void TextEditorView::hide_menu(bool hidden) {
    menu.hidden(hidden);

    // Only let the viewer be focused when the menu is
    // not shown, otherwise menu focus gets confusing.
    viewer.set_focusable(hidden);

    if (hidden)
        viewer.focus();

    viewer.redraw(true);
    set_dirty();
}

void TextEditorView::show_file_picker() {
    auto open_view = nav_.push<FileLoadView>("");
    open_view->on_changed = [this](std::filesystem::path path) {
        // Can't update the UI focus while the FileLoadView is still up.
        // Do this on a continuation instead of in on_changed.
        nav_.set_on_pop([this, p = std::move(path)]() {
            open_file(p);
            hide_menu();
        });
    };
}

void TextEditorView::show_edit_line() {
    auto str = file_->get_text(viewer.line(), 0, viewer.line_length());
    if (!str) {
        nav_.display_modal("Error", "Failed to get line text.");
        return;
    }

    edit_line_buffer_ = *std::move(str);

    text_prompt(
        nav_,
        edit_line_buffer_,
        viewer.col(),
        max_edit_length,
        [this](std::string& buffer) {
            auto range = file_->line_range(viewer.line());
            if (!range)
                return;

            prepare_for_write();
            file_->replace_range(*range, buffer);
        });
    nav_.set_on_pop([this]() {
        edit_line_buffer_.clear();
        refresh_ui();
        hide_menu(true);
    });
}

void TextEditorView::show_save_prompt(std::function<void()> continuation) {
    if (!file_dirty_) {
        if (continuation)
            continuation();
        return;
    }

    ui::show_save_prompt(
        nav_,
        [this]() { save_temp_file(); },
        continuation);
}

void TextEditorView::prepare_for_write() {
    file_dirty_ = true;

    if (has_temp_file_)
        return;

    // TODO: This would be nice to have but it causes a stack overflow in an ISR?
    // Painter p;
    // p.draw_string({2, 48}, Styles::yellow, "Creating temporary file...");

    // Copy to temp file on write.
    has_temp_file_ = true;
    delete_temp_file(path_);
    copy_file(path_, get_temp_path(path_));
    file_->assume_file(get_temp_path(path_));
}

void TextEditorView::save_temp_file() {
    if (file_dirty_) {
        ui::save_temp_file(path_);
        file_dirty_ = false;
    }
}

}  // namespace ui