// Copyright 2023 Kushview, LLC <info@kushview.net>
// SPDX-License-Identifier: GPL-3.0-or-later

#include "ui/console.hpp"
#include <element/ui/style.hpp>

namespace element {

static void setupEditor (TextEditor& editor)
{
    editor.setFont (FontOptions (Font::getDefaultMonospacedFontName(), 15.0f, 0));
}

//=============================================================================
class ConsoleBuffer : public TextEditor
{
public:
    ConsoleBuffer()
    {
        setupEditor (*this);
        setReadOnly (true);
        setMultiLine (true, false);
        setReturnKeyStartsNewLine (false);
        setJustification (Justification::bottomLeft);
        setWantsKeyboardFocus (false);
    }
};

//=============================================================================
class ConsolePrompt : public TextEditor
{
public:
    ConsolePrompt()
    {
        setupEditor (*this);
    }

    bool keyPressed (const KeyPress& k) override
    {
        if (k.getKeyCode() == KeyPress::upKey && onUpKey != nullptr)
            return onUpKey();
        else if (k.getKeyCode() == KeyPress::downKey && onDownKey != nullptr)
            return onDownKey();

        const auto mods = k.getModifiers();

        if (mods.isCtrlDown())
        {
            if (k.getKeyCode() == 'A')
            {
                moveCaretToTop (false);
                return true;
            }
            else if (k.getKeyCode() == 'E')
            {
                moveCaretToEnd (false);
                return true;
            }
        }

        return TextEditor::keyPressed (k);
    }

    std::function<bool()> onUpKey;
    std::function<bool()> onDownKey;
};

//=============================================================================
class Console::Content : public Component
{
public:
    Content (Console& o)
        : owner (o)
    {
        addAndMakeVisible (buffer);
        buffer.setLookAndFeel (&style);

        addAndMakeVisible (prefixLabel);
        prefixLabel.setText (prefixText, dontSendNotification);
        prefixLabel.setFont (prompt.getFont().withHeight (12.f));
        prefixLabel.setJustificationType (Justification::centred);

        addAndMakeVisible (prompt);
        prompt.setLookAndFeel (&style);

        prompt.onUpKey = [this]() -> bool {
            historyPos = jmax (0, historyPos - 1);
            loadHistoryItem (historyPos);
            return true;
        };

        prompt.onDownKey = [this]() -> bool {
            historyPos = jmin (history.size(), historyPos + 1);
            loadHistoryItem (historyPos);
            return true;
        };

        prompt.onTextChange = []() {};

        prompt.onReturnKey = [this] {
            auto text = prompt.getText();
            if (text.isEmpty())
                return;
            prompt.setText ({}, dontSendNotification);
            addToHistory (text);
            owner.handleTextEntry (text);
        };
    }

    ~Content()
    {
        buffer.setLookAndFeel (nullptr);
        prompt.setLookAndFeel (nullptr);
    }

    void clearBuffer()
    {
        buffer.clear();
        buffer.moveCaretToEnd();
    }

    void clearHistory()
    {
        history.clear();
        historyPos = history.size();
    }

    void addText (const String& text, bool prefix)
    {
        String line = prefix ? prefixText : String();
        if (line.isNotEmpty())
            line << " ";
        line << text;

        buffer.moveCaretToEnd();
        buffer.insertTextAtCaret (line.trimEnd());
        buffer.insertTextAtCaret (newLine);
        buffer.moveCaretToEnd();
    }

    void setPromptVisible (bool visible)
    {
        if (visible == prompt.isVisible())
            return;
        prompt.setVisible (visible);
        prefixLabel.setVisible (visible);
        resized();
    }

    void resized() override
    {
        auto r1 = getLocalBounds();
        if (prompt.isVisible())
        {
            auto r2 = r1.removeFromBottom (23);
            prefixLabel.setBounds (r2.removeFromLeft (24));
            prompt.setBounds (r2);
        }
        buffer.setBounds (r1);
    }

private:
    Console& owner;
    ConsoleBuffer buffer;
    Label prefixLabel;
    String prefixText { ">" };
    ConsolePrompt prompt;
    StringArray history;
    int historyPos { 0 };

    void addToHistory (const String& text)
    {
        if (history.isEmpty() || (history.size() > 0 && text != history.getReference (history.size() - 1)))
            history.add (text);
        if (history.size() > 100)
            history.remove (0);
        historyPos = history.size();
    }

    void loadHistoryItem (int index)
    {
        if (! isPositiveAndBelow (index, history.size()))
            return;
        if (prompt.getText() == history.getReference (index))
            return;
        prompt.setText (history.getReference (index), dontSendNotification);
        prompt.moveCaretToEnd();
    }

    struct Style : public element::LookAndFeel_E1
    {
        void fillTextEditorBackground (Graphics& g, int w, int h, TextEditor& e) override
        {
            LookAndFeel_E1::fillTextEditorBackground (g, w, h, e);
        }

        void drawTextEditorOutline (Graphics&, int, int, TextEditor&) override {}

        CaretComponent* createCaretComponent (Component* keyFocusOwner) override
        {
            return new CaretComponent (nullptr);
        }
    } style;
};

//=============================================================================
Console::Console (const String& name)
{
    setName (name);
    setOpaque (true);
    content.reset (new Content (*this));
    addAndMakeVisible (content.get());
    setSize (100, 100);
}

Console::~Console()
{
    content.reset();
}

void Console::clear (bool buffer, bool history)
{
    if (buffer)
        content->clearBuffer();
    if (history)
        content->clearHistory();
}

void Console::setPromptVisible (bool visible)
{
    content->setPromptVisible (visible);
}

void Console::addText (const String& text, bool prefix)
{
    content->addText (text, prefix);
}

void Console::textEntered (const String& text)
{
    addText (text, true);
}

void Console::resized()
{
    content->setBounds (getLocalBounds());
}

void Console::paint (Graphics& g)
{
    g.fillAll (findColour (Style::contentBackgroundColorId));
}

void Console::handleTextEntry (const String& text)
{
    textEntered (text);
}

} // namespace element
