#include "QskInputCompositionModel.h"

#include <QGuiApplication>
#include <QInputMethodEvent>
#include <QTextCharFormat>
#include <QWindow>

#include <unordered_map>

static QString qskKeyString( int code )
{
    // Special case entry codes here, else default to the symbol
    switch ( code )
    {
        case Qt::Key_Shift:
        case Qt::Key_CapsLock:
        case Qt::Key_Mode_switch:
        case Qt::Key_Backspace:
        case Qt::Key_Muhenkan:
            return QString();
        case Qt::Key_Return:
        case Qt::Key_Kanji:
            return QChar(QChar::CarriageReturn);
        case Qt::Key_Space:
            return QChar(QChar::Space);
        default:
            break;
    }
    return QChar(code);
}

class QskInputCompositionModel::PrivateData
{
public:
    // QInputMethod
    QString preedit;
    QTextCharFormat preeditFormat;
    QList< QInputMethodEvent::Attribute > preeditAttributes;

    int groupIndex;
};

static inline void sendCompositionEvent( QInputMethodEvent* e )
{
    if ( auto focusObject = QGuiApplication::focusObject() )
        QCoreApplication::sendEvent( focusObject, e );
}

QskInputCompositionModel::QskInputCompositionModel():
    m_data( new PrivateData )
{
    m_data->groupIndex = 0;

    m_data->preeditFormat.setFontUnderline(true);
    m_data->preeditAttributes.append( QInputMethodEvent::Attribute(
            QInputMethodEvent::TextFormat, 0, 0, m_data->preeditFormat ) );
}

QskInputCompositionModel::~QskInputCompositionModel()
{
}

void QskInputCompositionModel::composeKey( Qt::Key key )
{
    auto inputMethod = QGuiApplication::inputMethod();
    if ( !inputMethod )
        return;

    auto focusObject = QGuiApplication::focusObject();
    if ( !focusObject )
        return;

    QInputMethodQueryEvent queryEvent(
        Qt::ImSurroundingText | Qt::ImMaximumTextLength | Qt::ImHints );
    QCoreApplication::sendEvent( focusObject, &queryEvent );
    const auto hints = static_cast< Qt::InputMethodHints >(
            queryEvent.value( Qt::ImHints ).toInt() );
    const int maxLength = queryEvent.value( Qt::ImMaximumTextLength ).toInt();
    const int currentLength = queryEvent.value( Qt::ImSurroundingText ).toString().length();

    int spaceLeft = -1;
    if ( !( hints& Qt::ImhMultiLine ) && maxLength > 0 )
        spaceLeft = maxLength - currentLength;

    switch ( key )
    {
        case Qt::Key_Backspace:
        case Qt::Key_Muhenkan:
        {
            backspace();
            return;
        }
        case Qt::Key_Space:
        {
            if ( !spaceLeft )
                return;

            if ( !m_data->preedit.isEmpty() )
            {
                if ( candidateCount() > 0 ) // Commit first candidate
                    commit( QChar( candidate( 0 ) ) );
                else // Commit what is in the buffer
                    commit( m_data->preedit.left( spaceLeft ) );
            }
            else
            {
                commit( qskKeyString(key) );
            }
            return;
        }
        case Qt::Key_Return:
        {
            if ( !spaceLeft )
                return;

            // Commit what is in the buffer
            if ( !m_data->preedit.isEmpty() )
                commit( m_data->preedit.left( spaceLeft ) );
            else if ( hints & Qt::ImhMultiLine )
                commit( qskKeyString( key ) );

            return;
        }
        case Qt::Key_Left:
        case Qt::Key_Right:
        {
            moveCursor( key );
            return;
        }
        default:
        {
            if ( !spaceLeft )
                return;
        }
    }

    if ( hints & Qt::ImhHiddenText )
    {
        commit( qskKeyString( key ) );
        return;
    }

    const auto firstCandidate = candidateCount() > 0 ? QChar( candidate( 0 ) ) : QString();
    const auto oldPreedit = m_data->preedit;
    m_data->preedit += qskKeyString( key );
    auto displayPreedit = polishPreedit( m_data->preedit );

    // If there is no intermediate, decide between committing the first candidate and skipping
    if ( !hasIntermediate() )
    {
        // Skip preedit phase if there are no candidates/intermediates
        if ( firstCandidate.isEmpty() )
        {
            commit( oldPreedit.left( spaceLeft ) );
            spaceLeft -= oldPreedit.leftRef( spaceLeft ).length();
        }
        else
        {
            commit( firstCandidate );
            --spaceLeft;
        }

        if ( !spaceLeft )
            return;

        m_data->preedit = qskKeyString( key );
        displayPreedit = polishPreedit( m_data->preedit );
        if ( !hasIntermediate() )
        {
            commit(m_data->preedit);
            return;
        }
    }

    m_data->preeditAttributes.first().length = displayPreedit.length();
    QInputMethodEvent e(displayPreedit, m_data->preeditAttributes);
    sendCompositionEvent(&e);
}

void QskInputCompositionModel::clearPreedit()
{
    m_data->preedit.clear();
    polishPreedit( m_data->preedit );
}

int QskInputCompositionModel::candidateCount() const
{
    return 0;
}

Qt::Key QskInputCompositionModel::candidate( int ) const
{
    return Qt::Key( 0 );
}

// This method is called before displaying a new preedit string. It can be used
// to return a modified preedit string which is not stored as the underlying
// preedit text. This allows for marking up the preedit text without changing the
// data model. If the actual text needs to be modified, it is safe to call
// setPreeditText() here.
QString QskInputCompositionModel::polishPreedit( const QString& preedit )
{
    return preedit;
}

void QskInputCompositionModel::commit( const QString& text )
{
    QInputMethodEvent e( QString(), { } );
    e.setCommitString( text );
    sendCompositionEvent( &e );
    m_data->preedit.clear();
    polishPreedit( m_data->preedit );
}

void QskInputCompositionModel::commitCandidate( int index )
{
    commit( qskKeyString( candidate( index ) ) );
}

void QskInputCompositionModel::backspace()
{
    auto focusWindow = QGuiApplication::focusWindow();
    if ( !focusWindow )
        return;

    if ( !m_data->preedit.isEmpty() )
    {
        m_data->preedit.chop(1);
    }
    else
    {
        // Backspace one character only if preedit was inactive
        QKeyEvent keyPress( QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier );
        QKeyEvent keyRelease( QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier );
        QCoreApplication::sendEvent( focusWindow, &keyPress );
        QCoreApplication::sendEvent( focusWindow, &keyRelease );
        return;
    }

    const QString displayText = polishPreedit( m_data->preedit );
    m_data->preeditAttributes.first().length = displayText.length();
    QInputMethodEvent e( displayText, m_data->preeditAttributes );
    sendCompositionEvent( &e );
}

void QskInputCompositionModel::moveCursor( Qt::Key key )
{
    if ( key != Qt::Key_Left && key != Qt::Key_Right )
        return;

    auto focusWindow = QGuiApplication::focusWindow();
    if ( !focusWindow )
        return;

    // Moving cursor is disabled when preedit is active.
    if ( !m_data->preedit.isEmpty() )
        return;

    QKeyEvent moveCursorPress( QEvent::KeyPress, key, Qt::NoModifier );
    QKeyEvent moveCursorRelease( QEvent::KeyRelease, key,  Qt::NoModifier );
    QCoreApplication::sendEvent( focusWindow, &moveCursorPress );
    QCoreApplication::sendEvent( focusWindow, &moveCursorRelease );
}

bool QskInputCompositionModel::hasIntermediate() const
{
    return false;
}

bool QskInputCompositionModel::isComposable(const QStringRef& preedit) const
{
    Q_UNUSED(preedit);
    return false;
}

int QskInputCompositionModel::groupIndex() const
{
    return m_data->groupIndex;
}

void QskInputCompositionModel::setGroupIndex( int groupIndex )
{
    if ( groupIndex == m_data->groupIndex )
        return;

    m_data->groupIndex = groupIndex;
    const QString displayText = polishPreedit( m_data->preedit );
    m_data->preeditAttributes.first().length = displayText.length();
    QInputMethodEvent e( displayText, m_data->preeditAttributes );
    sendCompositionEvent( &e );
}

QVector< Qt::Key > QskInputCompositionModel::groups() const
{
    return QVector< Qt::Key >();
}

bool QskInputCompositionModel::nextGroupIndex(int& index, bool forward) const
{
    Q_UNUSED(index);
    Q_UNUSED(forward);
    return false;
}

#include "moc_QskInputCompositionModel.cpp"
