/*
 * Copyright (c) 2021, the SerenityOS developers.
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include <AK/Optional.h>
#include <LibCore/Object.h>
#include <LibGUI/EditingEngine.h>
#include <LibGUI/TextRange.h>

namespace GUI {

// Wrapper over TextPosition that makes it easier to move it around as a cursor,
// and to get the current line or character.
class VimCursor {
public:
    VimCursor(TextEditor& editor, TextPosition initial_position, bool forwards)
        : m_editor(editor)
        , m_position(initial_position)
        , m_forwards(forwards)
    {
    }

    void move_forwards();
    void move_backwards();

    // Move a single character in the current direction.
    void move();
    // Move a single character in reverse.
    void move_reverse();
    // Peek a single character in the current direction.
    u32 peek();
    // Peek a single character in reverse.
    u32 peek_reverse();
    // Get the character the cursor is currently on.
    u32 current_char();
    // Get the line the cursor is currently on.
    TextDocumentLine& current_line();
    // Get the current position.
    TextPosition& current_position() { return m_position; }

    // Did we hit the edge of the document?
    bool hit_edge() { return m_hit_edge; }
    // Will the next move cross a line boundary?
    bool will_cross_line_boundary();
    // Did we cross a line boundary?
    bool crossed_line_boundary() { return m_crossed_line_boundary; }
    // Are we on an empty line?
    bool on_empty_line();
    // Are we going forwards?
    bool forwards() { return m_forwards; }

private:
    TextEditor& m_editor;
    TextPosition m_position;
    bool m_forwards;

    u32 m_cached_char { 0 };

    bool m_hit_edge { false };
    bool m_crossed_line_boundary { false };
};

class VimMotion {
public:
    enum class Unit {
        // The motion isn't complete yet, or was invalid.
        Unknown,
        // Document. Anything non-negative is counted as G while anything else is gg.
        Document,
        // Lines.
        Line,
        // A sequence of letters, digits and underscores, or a sequence of other
        // non-blank characters separated by whitespace.
        Word,
        // A sequence of non-blank characters separated by whitespace.
        // This is how Vim separates w from W.
        WORD,
        // End of a word. This is basically the same as a word but it doesn't
        // trim the spaces at the end.
        EndOfWord,
        // End of a WORD.
        EndOfWORD,
        // Characters (or Unicode code points based on how pedantic you want to
        // get).
        Character,
        // Used for find-mode.
        Find
    };
    enum class FindMode {
        /// Find mode is not enabled.
        None,
        /// Finding until the given character.
        To,
        /// Finding through the given character.
        Find
    };

    void add_key_code(KeyCode key, bool ctrl, bool shift, bool alt);
    Optional<TextRange> get_range(class VimEditingEngine& engine, bool normalize_for_position = false);
    Optional<TextPosition> get_position(VimEditingEngine& engine, bool in_visual_mode = false);
    void reset();

    /// Returns whether the motion should consume the next character no matter what.
    /// Used for f and t motions.
    bool should_consume_next_character() { return m_should_consume_next_character; }
    bool is_complete() { return m_is_complete; }
    bool is_cancelled() { return m_is_complete && m_unit == Unit::Unknown; }
    Unit unit() { return m_unit; }
    int amount() { return m_amount; }

    // FIXME: come up with a better way to signal start/end of line than sentinels?
    static constexpr int START_OF_LINE = NumericLimits<int>::min();
    static constexpr int START_OF_NON_WHITESPACE = NumericLimits<int>::min() + 1;
    static constexpr int END_OF_LINE = NumericLimits<int>::max();

private:
    void calculate_document_range(TextEditor&);
    void calculate_line_range(TextEditor&, bool normalize_for_position);
    void calculate_word_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_WORD_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_character_range(VimCursor&, int amount, bool normalize_for_position);
    void calculate_find_range(VimCursor&, int amount);

    Unit m_unit { Unit::Unknown };
    int m_amount { 0 };
    bool m_is_complete { false };
    bool m_guirky_mode { false };
    bool m_should_consume_next_character { false };

    FindMode m_find_mode { FindMode::None };
    u32 m_next_character { 0 };

    size_t m_start_line { 0 };
    size_t m_start_column { 0 };
    size_t m_end_line { 0 };
    size_t m_end_column { 0 };
};

class VimEditingEngine final : public EditingEngine {

public:
    virtual CursorWidth cursor_width() const override;

    virtual bool on_key(const KeyEvent& event) override;

private:
    enum VimMode {
        Normal,
        Insert,
        Visual
    };

    enum YankType {
        Line,
        Selection
    };

    VimMode m_vim_mode { VimMode::Normal };
    VimMotion m_motion;

    YankType m_yank_type {};
    String m_yank_buffer {};
    void yank(YankType);
    void yank(TextRange);
    void put();

    TextPosition m_selection_start_position = {};
    void update_selection_on_cursor_move();
    void clamp_cursor_position();
    void clear_visual_mode_data();

    KeyCode m_previous_key {};
    void switch_to_normal_mode();
    void switch_to_insert_mode();
    void switch_to_visual_mode();
    void move_half_page_up();
    void move_half_page_down();
    void move_to_previous_empty_lines_block();
    void move_to_next_empty_lines_block();

    bool on_key_in_insert_mode(const KeyEvent& event);
    bool on_key_in_normal_mode(const KeyEvent& event);
    bool on_key_in_visual_mode(const KeyEvent& event);
};

}
