// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#ifndef QQUICKTEXTINPUT_P_P_H
#define QQUICKTEXTINPUT_P_P_H

#include "qquicktextinput_p.h"
#include "qquicktext_p.h"
#include "qquickimplicitsizeitem_p_p.h"
#include "qquicktextutil_p.h"

#include <QtQml/qqml.h>
#include <QtCore/qelapsedtimer.h>
#include <QtCore/qpointer.h>
#include <QtCore/qbasictimer.h>
#include <QtGui/qclipboard.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qpalette.h>
#include <QtGui/qtextlayout.h>
#include <QtGui/qstylehints.h>
#include <private/qlazilyallocated_p.h>

#include "qplatformdefs.h"

#include <memory>

//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API.  It exists purely as an
// implementation detail.  This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.

QT_BEGIN_NAMESPACE

class QSGInternalTextNode;
class QInputControl;

class Q_QUICK_EXPORT QQuickTextInputPrivate : public QQuickImplicitSizeItemPrivate
{
public:
    Q_DECLARE_PUBLIC(QQuickTextInput)

    typedef QQuickTextInput Public;

    struct ExtraData {
        ExtraData();

        qreal padding;
        qreal topPadding;
        qreal leftPadding;
        qreal rightPadding;
        qreal bottomPadding;
        bool explicitTopPadding : 1;
        bool explicitLeftPadding : 1;
        bool explicitRightPadding : 1;
        bool explicitBottomPadding : 1;
        bool implicitResize : 1;
    };
    QLazilyAllocated<ExtraData> extra;

    QQuickTextInputPrivate()
        : hscroll(0)
        , vscroll(0)
        , cursorItem(nullptr)
        , textNode(nullptr)
        , m_maskData(nullptr)
        , color(QRgb(0xFF000000))
        , selectionColor(QRgb(0xFF000080))
        , selectedTextColor(QRgb(0xFFFFFFFF))
        , m_cursor(0)
#if QT_CONFIG(im)
        , m_preeditCursor(0)
        , m_undoPreeditState(-1)
#endif
        , m_blinkEnabled(false)
        , m_blinkTimer(0)
        , m_maxLength(32767)
        , m_lastCursorPos(-1)
        , m_undoState(0)
        , m_selstart(0)
        , m_selend(0)
#if QT_CONFIG(im)
        , inputMethodHints(Qt::ImhNone)
#endif
        , hAlign(QQuickTextInput::AlignLeft)
        , vAlign(QQuickTextInput::AlignTop)
        , wrapMode(QQuickTextInput::NoWrap)
        , m_echoMode(QQuickTextInput::Normal)
        , renderType(QQuickTextUtil::textRenderType<QQuickTextInput>())
        , updateType(UpdatePaintNode)
        , mouseSelectionMode(QQuickTextInput::SelectCharacters)
        , m_layoutDirection(Qt::LayoutDirectionAuto)
        , m_passwordCharacter(QGuiApplication::styleHints()->passwordMaskCharacter())
        , m_passwordMaskDelay(QGuiApplication::styleHints()->passwordMaskDelay())
        , focusOnPress(true)
        , cursorVisible(false)
        , cursorPending(false)
        , autoScroll(true)
        , selectByMouse(true)
        , canPaste(false)
        , canPasteValid(false)
        , canUndo(false)
        , canRedo(false)
        , hAlignImplicit(true)
        , selectPressed(false)
        , hadSelectionOnMousePress(false)
        , textLayoutDirty(true)
        , persistentSelection(false)
        , hasImState(false)
        , m_separator(0)
        , m_readOnly(0)
        , m_textDirty(0)
#if QT_CONFIG(im)
        , m_preeditDirty(0)
#endif
        , m_selDirty(0)
        , m_validInput(1)
        , m_acceptableInput(1)
        , m_blinkStatus(0)
        , m_passwordEchoEditing(false)
        , inLayout(false)
        , requireImplicitWidth(false)
        , overwriteMode(false)
        , containsUnscalableGlyphs(false)
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
        , selectByTouchDrag(false)
#endif
    {
    }

    ~QQuickTextInputPrivate()
    {
        // If this control is used for password input, we don't want the
        // password data to stay in the process memory, therefore we need
        // to zero it out
        if (m_echoMode != QQuickTextInput::Normal)
            m_text.fill(u'\0');
    }

    void init();
    void cancelInput();
    void startCreatingCursor();
    void ensureVisible(int position, int preeditCursor = 0, int preeditLength = 0);
    void updateHorizontalScroll();
    void updateVerticalScroll();
    bool determineHorizontalAlignment();
    bool setHAlign(QQuickTextInput::HAlignment, bool forceAlign = false);
    void mirrorChange() override;
    bool sendMouseEventToInputContext(QMouseEvent *event);
#if QT_CONFIG(im)
    Qt::InputMethodHints effectiveInputMethodHints() const;
#endif
    void handleFocusEvent(QFocusEvent *event);

    struct MaskInputData {
        enum Casemode { NoCaseMode, Upper, Lower };
        QChar maskChar; // either the separator char or the inputmask
        bool separator;
        Casemode caseMode;
    };

    // undo/redo handling
    enum CommandType { Separator, Insert, Remove, Delete, RemoveSelection, DeleteSelection, SetSelection };
    struct Command {
        inline Command() {}
        inline Command(CommandType t, int p, QChar c, int ss, int se) : type(t),uc(c),pos(p),selStart(ss),selEnd(se) {}
        uint type : 4;
        QChar uc;
        int pos, selStart, selEnd;
    };

    enum DrawFlags {
        DrawText = 0x01,
        DrawSelections = 0x02,
        DrawCursor = 0x04,
        DrawAll = DrawText | DrawSelections | DrawCursor
    };

    QElapsedTimer tripleClickTimer;
    QSizeF contentSize;
    QPointF pressPos;
    QPointF tripleClickStartPoint;

    QPointer<QQmlComponent> cursorComponent;
#if QT_CONFIG(validator)
    QPointer<QValidator> m_validator;
#endif

    qreal hscroll;
    qreal vscroll;

    QTextLayout m_textLayout;
    QString m_text;
    QString m_inputMask;
    QString m_cancelText;
    QFont font;
    QFont sourceFont;

    QQuickItem *cursorItem;
    QSGInternalTextNode *textNode;
    std::unique_ptr<MaskInputData[]> m_maskData;
    QInputControl *m_inputControl;

    QList<int> m_transactions;
    QVector<Command> m_history;

    QColor color;
    QColor selectionColor;
    QColor selectedTextColor;

    QBasicTimer m_passwordEchoTimer;
    int lastSelectionStart;
    int lastSelectionEnd;
    int m_cursor;
#if QT_CONFIG(im)
    int m_preeditCursor;
    int m_undoPreeditState;
#endif
    bool m_blinkEnabled;
    int m_blinkTimer;
    int m_maxLength;
    int m_lastCursorPos;
    int m_undoState;
    int m_selstart;
    int m_selend;

    enum UpdateType {
        UpdateNone,
        UpdateOnlyPreprocess,
        UpdatePaintNode
    };

#if QT_CONFIG(im)
    Qt::InputMethodHints inputMethodHints;
#endif
    QQuickTextInput::HAlignment hAlign;
    QQuickTextInput::VAlignment vAlign;
    QQuickTextInput::WrapMode wrapMode;
    QQuickTextInput::EchoMode m_echoMode;
    QQuickTextInput::RenderType renderType;
    UpdateType updateType;
    QQuickTextInput::SelectionMode mouseSelectionMode;
    Qt::LayoutDirection m_layoutDirection;

    QChar m_blank;
    QChar m_passwordCharacter;
    int m_passwordMaskDelay;

    bool focusOnPress:1;
    bool cursorVisible:1;
    bool cursorPending:1;
    bool autoScroll:1;
    bool selectByMouse:1;
    bool canPaste:1;
    bool canPasteValid:1;
    bool canUndo:1;
    bool canRedo:1;
    bool hAlignImplicit:1;
    bool selectPressed:1;
    bool hadSelectionOnMousePress:1;
    bool textLayoutDirty:1;
    bool persistentSelection:1;
    bool hasImState : 1;
    bool m_separator : 1;
    bool m_readOnly : 1;
    bool m_textDirty : 1;
#if QT_CONFIG(im)
    bool m_preeditDirty : 1;
#endif
    bool m_selDirty : 1;
    bool m_validInput : 1;
    bool m_acceptableInput : 1;
    bool m_blinkStatus : 1;
    bool m_passwordEchoEditing : 1;
    bool inLayout:1;
    bool requireImplicitWidth:1;
    bool overwriteMode:1;
    bool containsUnscalableGlyphs:1;
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
    bool selectByTouchDrag:1;
#endif

    static inline QQuickTextInputPrivate *get(QQuickTextInput *t) {
        return t->d_func();
    }
    bool hasPendingTripleClick() const {
        return !tripleClickTimer.hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval());
    }

    void setNativeCursorEnabled(bool) {
        updateCursorBlinking();
    }

    int nextMaskBlank(int pos)
    {
        int c = findInMask(pos, true, false);
        m_separator |= (c != pos);
        return (c != -1 ?  c : m_maxLength);
    }

    int prevMaskBlank(int pos)
    {
        int c = findInMask(pos, false, false);
        m_separator |= (c != pos);
        return (c != -1 ? c : 0);
    }

    bool isUndoAvailable() const { return !m_readOnly && m_undoState; }
    bool isRedoAvailable() const { return !m_readOnly && m_undoState < (int)m_history.size(); }
    void clearUndo() {
            m_history.clear();
            m_undoState = 0;
#if QT_CONFIG(im)
            m_undoPreeditState = -1;
#endif
    }

    bool allSelected() const { return !m_text.isEmpty() && m_selstart == 0 && m_selend == (int)m_text.size(); }
    bool hasSelectedText() const { return !m_text.isEmpty() && m_selend > m_selstart; }

    void setSelection(int start, int length);

    inline QString selectedText() const { return hasSelectedText() ? m_text.mid(m_selstart, m_selend - m_selstart) : QString(); }
    QString textBeforeSelection() const { return hasSelectedText() ? m_text.left(m_selstart) : QString(); }
    QString textAfterSelection() const { return hasSelectedText() ? m_text.mid(m_selend) : QString(); }

    int selectionStart() const { return hasSelectedText() ? m_selstart : -1; }
    int selectionEnd() const { return hasSelectedText() ? m_selend : -1; }

    QRectF anchorRectangle() const;

    int positionAt(qreal x, qreal y, QTextLine::CursorPosition position) const;
    int positionAt(const QPointF &point, QTextLine::CursorPosition position = QTextLine::CursorBetweenCharacters) const {
        return positionAt(point.x(), point.y(), position);
    }

    void removeSelection()
    {
        int priorState = m_undoState;
        removeSelectedText();
        finishChange(priorState);
    }

    int start() const { return 0; }
    int end() const { return m_text.size(); }

    QString realText() const;

#if QT_CONFIG(clipboard)
    void copy(QClipboard::Mode mode = QClipboard::Clipboard) const;
    void paste(QClipboard::Mode mode = QClipboard::Clipboard);
#endif

#if QT_CONFIG(im)
    void commitPreedit();
    void cancelPreedit();
#endif

    Qt::CursorMoveStyle cursorMoveStyle() const { return m_textLayout.cursorMoveStyle(); }
    void setCursorMoveStyle(Qt::CursorMoveStyle style) { m_textLayout.setCursorMoveStyle(style); }

    void moveCursor(int pos, bool mark = false);
    void cursorForward(bool mark, int steps)
    {
        int c = m_cursor;
        if (steps > 0) {
            while (steps--)
                c = cursorMoveStyle() == Qt::VisualMoveStyle ? m_textLayout.rightCursorPosition(c)
                                                             : m_textLayout.nextCursorPosition(c);
        } else if (steps < 0) {
            while (steps++)
                c = cursorMoveStyle() == Qt::VisualMoveStyle ? m_textLayout.leftCursorPosition(c)
                                                             : m_textLayout.previousCursorPosition(c);
        }
        moveCursor(c, mark);
    }

    void cursorWordForward(bool mark) { moveCursor(m_textLayout.nextCursorPosition(m_cursor, QTextLayout::SkipWords), mark); }
    void cursorWordBackward(bool mark) { moveCursor(m_textLayout.previousCursorPosition(m_cursor, QTextLayout::SkipWords), mark); }

    void home(bool mark) { moveCursor(0, mark); }
    void end(bool mark) { moveCursor(q_func()->text().size(), mark); }

    void backspace();
    void del();
    void deselect() { internalDeselect(); finishChange(); }
    void selectAll() { m_selstart = m_selend = m_cursor = 0; moveCursor(m_text.size(), true); }

    void insert(const QString &);
    void clear();
    void selectWordAtPos(int);

    void setCursorPosition(int pos) { if (pos <= m_text.size()) moveCursor(qMax(0, pos)); }

    bool fixup();

    QString inputMask() const { return m_maskData ? m_inputMask + QLatin1Char(';') + m_blank : QString(); }
    void setInputMask(const QString &mask)
    {
        parseInputMask(mask);
        if (m_maskData)
            moveCursor(nextMaskBlank(0));
    }

    // input methods
#if QT_CONFIG(im)
    bool composeMode() const { return !m_textLayout.preeditAreaText().isEmpty(); }

    QString preeditAreaText() const { return m_textLayout.preeditAreaText(); }
#endif

    void updatePasswordEchoEditing(bool editing);

    void cancelPasswordEchoTimer() {
        m_passwordEchoTimer.stop();
    }

    Qt::LayoutDirection textDirection() const;
    Qt::LayoutDirection layoutDirection() const;
    void setLayoutDirection(Qt::LayoutDirection direction)
    {
        if (direction != m_layoutDirection) {
            m_layoutDirection = direction;
            updateDisplayText();
        }
    }

#if QT_CONFIG(im)
    void processInputMethodEvent(QInputMethodEvent *event);
#endif
    void processKeyEvent(QKeyEvent* ev);

    void setBlinkingCursorEnabled(bool enable);
    void updateCursorBlinking();

    void updateLayout();
    void updateBaselineOffset();

    qreal calculateImplicitWidthForText(const QString &text) const;
    qreal getImplicitWidth() const override;

    inline qreal padding() const { return extra.isAllocated() ? extra->padding : 0.0; }
    void setTopPadding(qreal value, bool reset = false);
    void setLeftPadding(qreal value, bool reset = false);
    void setRightPadding(qreal value, bool reset = false);
    void setBottomPadding(qreal value, bool reset = false);

    bool isImplicitResizeEnabled() const;
    void setImplicitResizeEnabled(bool enabled);

private:
    void removeSelectedText();
    void internalSetText(const QString &txt, int pos = -1, bool edited = true);
    void updateDisplayText(bool forceUpdate = false);

    void internalInsert(const QString &s);
    void internalDelete(bool wasBackspace = false);
    void internalRemove(int pos);

    inline void internalDeselect()
    {
        m_selDirty |= (m_selend > m_selstart);
        m_selstart = m_selend = 0;
    }

    void internalUndo(int until = -1);
    void internalRedo();
    void emitUndoRedoChanged();

    bool emitCursorPositionChanged();

    bool finishChange(int validateFromState = -1, bool update = false, bool edited = true);

    void addCommand(const Command& cmd);

    inline void separate() { m_separator = true; }

    bool separateSelection();
    void deleteStartOfWord();
    void deleteEndOfWord();
    void deleteEndOfLine();

    enum ValidatorState {
#if QT_CONFIG(validator)
        InvalidInput        = QValidator::Invalid,
        IntermediateInput   = QValidator::Intermediate,
        AcceptableInput     = QValidator::Acceptable
#else
        InvalidInput,
        IntermediateInput,
        AcceptableInput
#endif
    };

    // masking
    void parseInputMask(const QString &maskFields);
    bool isValidInput(QChar key, QChar mask) const;
    ValidatorState hasAcceptableInput(const QString &text) const;
    void checkIsValid();
    QString maskString(uint pos, const QString &str, bool clear = false) const;
    QString clearString(uint pos, uint len) const;
    QString stripString(const QString &str) const;
    int findInMask(int pos, bool forward, bool findSeparator, QChar searchChar = QChar()) const;
};

QT_END_NAMESPACE

#endif // QQUICKTEXTINPUT_P_P_H
