/**
 * Copyright (C) 2023-2026 Linaro Limited (or its affiliates). All rights reserved.
 * Copyright (C) 2012-2023 Arm Limited (or its affiliates).
 * All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */

#include <QApplication>
#include <QClipboard>
#include <QKeyEvent>
#include <QLocalSocket>
#include <QPainter>
#include <QScrollBar>
#include <QStyleOption>
#include <QTextBlock>
#include <QTextCodec>
#include <QTextDocument>
#include <QTextLayout>
#include "terminal.h"
#include "terminalutil.h"
#include "terminalwidget.h"

namespace {
    int FontPointSize = 10;
    int DefaultColumns = 80;
    int DefaultRows = 12;
}

TerminalWidget::TerminalWidget(Terminal *terminal, QWidget *parent) :
    QTextEdit(parent),
    mTerminal(0),
    mDecoder(0)
{
    init();
    setTerminal(terminal);
}

TerminalWidget::TerminalWidget(QWidget *parent) :
    QTextEdit(parent),
    mTerminal(0),
    mDecoder(0)
{
    init();
}

void TerminalWidget::init()
{
    setWordWrapMode(QTextOption::WrapAnywhere);
    setLineWrapMode(QTextEdit::FixedColumnWidth);
    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    setReadOnly(true);
    setCursorWidth(fontMetrics().horizontalAdvance(QLatin1Char('M')));
    setWindowTitle(tr("Terminal"));
    QTextCodec *codec = QTextCodec::codecForLocale();
    mDecoder = codec->makeDecoder();
    mTerminalCursor = QTextCursor(document());

    pasteAction = new QAction(tr("&Paste"), this);
    pasteAction->setShortcuts(QKeySequence::Paste);
    connect(pasteAction, &QAction::triggered,
            this,        &TerminalWidget::pasteFromClipboard);
}

TerminalWidget::~TerminalWidget()
{
    if (mDecoder)
        delete mDecoder;
}

void TerminalWidget::setTerminal(Terminal *terminal)
{
    // Disconnect from signals emitted by the old terminal 
    if (mTerminal) 
    {
        disconnect(mTerminal, &Terminal::readyRead, this, &TerminalWidget::terminalReadyRead);
    }

    mTerminal = terminal;
    if (mTerminal) {
        connect(mTerminal, &Terminal::windowSizeChanged, this, &TerminalWidget::windowSizeChanged);
        connect(mTerminal, &Terminal::readyRead, this, &TerminalWidget::terminalReadyRead);
        connect(mTerminal, &Terminal::processStarted, this, &TerminalWidget::processStarted);
        connect(mTerminal, &Terminal::processFinished, this, &TerminalWidget::processFinished);
        windowSizeChanged(mTerminal->columns(), mTerminal->rows());
    }
}

Terminal *TerminalWidget::terminal() const
{
    return mTerminal;
}

QSize TerminalWidget::sizeHint() const
{
    return minimumSizeHint();
}

QSize TerminalWidget::minimumSizeHint() const
{
    ensurePolished();
    int margin = document()->documentMargin();
    QFontMetrics fm(fontMetrics());
    int pixelWidth = fm.horizontalAdvance(QLatin1Char('M')) * (mTerminal ? mTerminal->columns() : DefaultColumns);
    int pixelHeight = fm.height() * (mTerminal ? mTerminal->rows() : DefaultRows);
    int vsbExt = verticalScrollBar()->sizeHint().width();
    int extra = 2 * frameWidth();
    QStyleOption opt;
    opt.initFrom(this);
    if ((frameStyle() != QFrame::NoFrame)
        && style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &opt, this)) {
        extra += style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, &opt, this);
    }
    int frameWidth = vsbExt + extra;
    int frameHeight = extra;
    return QSize(margin * 2 + pixelWidth + frameWidth,
                 margin * 2 + pixelHeight + frameHeight);
}

void TerminalWidget::windowSizeChanged(int cols, int)
{
    setLineWrapColumnOrWidth(cols);
    updateGeometry();
}

void TerminalWidget::appendBytes(const char *c, int len)
{
    QString s(mDecoder->toUnicode(c, len));
    if (s.isEmpty())
        return;
    appendString(s);
}

void TerminalWidget::appendString(const QString& s)
{
    bool stickToBottom = (verticalScrollBar()->value() + 1 >= verticalScrollBar()->maximum());
    mTerminalCursor.beginEditBlock();
    mTerminalCursor.insertText(s);
    mTerminalCursor.endEditBlock();
    if (stickToBottom)
        verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}

void TerminalWidget::terminalReadyRead()
{
    QByteArray ba(mBuf + mTerminal->readAll());
    mBuf.clear();
    const char *bytes = ba.constData();
    const char *end = bytes + ba.size();
    const char *normalBytes = bytes;
    for (const char *p = bytes; p < end;) {
        char c = *p;
        if ((c < 32 && c != 10 && c != 13) || c == 127) {
            if (p > normalBytes)
                appendBytes(normalBytes, p - normalBytes);
            ++p;
            switch (c) {
            case 7: // BEL
                break;
            case 8:
            case 127:
                mTerminalCursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);
                break;
            case 27:
                if (p == end) {
                    mBuf.append(c);
                } else {
                    c = *p++;
                    switch (c) {
                    case '[':
                        if (p == end) {
                            mBuf.append(p - 2, 2);
                        } else {
                            c = *p++;
                            switch(c) {
                            case 'K': {
                                QTextCursor cursor(mTerminalCursor);
                                cursor.setPosition(cursor.position(), QTextCursor::MoveAnchor);
                                cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
                                cursor.removeSelectedText();
                                break;
                            }
                            default:
                                //qDebug("Unsupported escape sequence <ESC> %c %c", *(p - 2), c);
                                break;
                            }
                        }
                        break;
                    default:
                        //qDebug("Unsupported escape sequence <ESC> %c", c);
                        break;
                    }
                }
                break;
            default:
                //qDebug("Unsupported control code %d", (int) c);
                break;
            }
            normalBytes = p;
        } else {
            ++p;
        }
    }
    if (end > normalBytes)
        appendBytes(normalBytes, end - normalBytes);
}

void TerminalWidget::contextMenuEvent(QContextMenuEvent *event)
{
    mMenu = createStandardContextMenu();

    // Insert "Paste" action after "Copy"
    const QList<QAction*> actionsList = mMenu->actions();
    QListIterator<QAction*> it(actionsList);
    QAction *itemToInsertBefore = NULL;
    for (it.toBack(); it.hasPrevious(); itemToInsertBefore = it.previous())
    {
        if(it.peekPrevious()->text().contains(tr("Copy")))
            break;
    }
    mMenu->insertAction(itemToInsertBefore, pasteAction);

    mMenu->exec(event->globalPos()); // block until menu is closed
    delete mMenu;
}

void TerminalWidget::pasteFromClipboard()
{
    mTerminal->write(QApplication::clipboard()->text().toUtf8());
}

void TerminalWidget::keyPressEvent(QKeyEvent *event)
{
    if (!mTerminal)
        return;

    // Handle keyboard shortcut pasting (this must occur before checking event->text()
    // as it "can be an empty string in cases where modifier keys, such as Shift, Control,
    // Alt, and Meta, are being pressed or released.", see QKeyEvent docs)
    if (event->matches(QKeySequence::Paste))
    {
        // Don't forward paste to the QTextEdit, we want to write to the terminal,
        // not actually enter text into the textbox.
        mTerminal->write(QApplication::clipboard()->text().toUtf8());
    }        
    else if(event->modifiers() == Qt::MetaModifier && event->key() == Qt::Key_C) 
    {
        // Re-implement ctrl (Meta) C on OS X as a real ctrl-c interrupt character.
        // This also ends up working as ctrl-shift-c on Linux and Windows, but there
        // is no reason not to have that behaviour.
        mTerminal->write(QByteArray("\003"));
    }
    else if(event->matches(QKeySequence::Copy) || 
            event->matches(QKeySequence::SelectAll) ||
            event->matches(QKeySequence::Cut)) 
    {
        // Forward shortcuts that need to be handled by the text box to the 
        // base class, not the terminal.
        QTextEdit::keyPressEvent(event);
    }
    else
    {
        // All other key sequences not handled above are sent to the terminal.
        QString s(event->text());
        if (s.isEmpty())
            return;
        QByteArray ba(s.toLocal8Bit());
        if (ba == "\015" || event->key() == Qt::Key_Enter) // CR or numpad enter
            ba = "\012"; // NL
        if (ba == "\012")
            emit returnPressed();

        // #29239 - backspace not working on (not only mac) for remote terminal password
        if( event->key() == Qt::Key_Backspace) // backspace keycode is octal "\010"
        {
            ba = "\177"; // delete
        }

        mTerminal->write(ba);
    }
}

void TerminalWidget::paintEvent(QPaintEvent *e)
{
    QTextEdit::paintEvent(e);
    QPainter p(viewport());
    const int xOffset = horizontalScrollBar()->value();
    const int yOffset = verticalScrollBar()->value();
    p.translate(-xOffset, -yOffset);
    QTextLayout *layout = mTerminalCursor.block().layout();
    layout->drawCursor(&p, QPointF(0, 0), mTerminalCursor.position(), cursorWidth());
}

void TerminalWidget::processStarted(const QString& commandLine)
{
    appendString(commandLine + QLatin1Char('\n'));
}

void TerminalWidget::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (exitStatus == QProcess::NormalExit)
        appendString(QLatin1Char('\n' ) + tr("[process exited with exit code %1]").arg(exitCode) + QLatin1Char('\n'));
    else
        appendString(QLatin1Char('\n' ) + tr("[process exited abnormally]") + QLatin1Char('\n'));
}
