/******************************************************************************
 * QSkinny - Copyright (C) The authors
 *           SPDX-License-Identifier: BSD-3-Clause
 *****************************************************************************/

#include "QskVirtualKeyboard.h"
#include "QskPushButton.h"

#include <qguiapplication.h>
#include <qset.h>
#include <qstylehints.h>

namespace
{
    class Button : public QskPushButton
    {
      public:
        Button( QskVirtualKeyboard* parent )
            : QskPushButton( parent )
        {
            setFocusPolicy( Qt::TabFocus );

            setSubcontrolProxy( QskPushButton::Panel, QskVirtualKeyboard::ButtonPanel );
            setSubcontrolProxy( QskPushButton::Text, QskVirtualKeyboard::ButtonText );
        }

        int key() const
        {
            return m_key;
        }

        void setKey( int key )
        {
            m_key = key;
        }

      private:
        int m_key = 0;
    };

    static bool qskIsAutorepeat( int key )
    {
        return (
            ( key != Qt::Key_Return ) &&
            ( key != Qt::Key_Enter ) &&
            ( key != Qt::Key_Shift ) &&
            ( key != Qt::Key_CapsLock ) &&
            ( key != Qt::Key_Mode_switch ) );
    }
}


QSK_SUBCONTROL( QskVirtualKeyboard, Panel )
QSK_SUBCONTROL( QskVirtualKeyboard, ButtonPanel )
QSK_SUBCONTROL( QskVirtualKeyboard, ButtonText )

class QskVirtualKeyboard::PrivateData
{
  public:
    int rowCount = 5;
    int columnCount = 12;

    QskVirtualKeyboardLayouts layouts;
    const QskVirtualKeyboardLayouts::Layout* currentLayout = nullptr;
    QskVirtualKeyboard::Mode mode = QskVirtualKeyboard::LowercaseMode;

    QVector< Button* > keyButtons;
    QSet< int > keyCodes;
};

QskVirtualKeyboard::QskVirtualKeyboard( QQuickItem* parent )
    : Inherited( parent )
    , m_data( new PrivateData )
{
    setPolishOnResize( true );
    initSizePolicy( QskSizePolicy::Expanding, QskSizePolicy::Fixed );

#define LOWER( x ) int( x + 32 ) // Convert an uppercase key to lowercase
    m_data->layouts =
    {
#include "QskVirtualKeyboardLayouts.hpp"
    };
#undef LOWER

    ensureButtons();

    connect( this, &QskControl::localeChanged,
        this, &QskVirtualKeyboard::updateLocale );

    updateLocale( locale() );

    setSubcontrolProxy( QskBox::Panel, Panel );
}

QskVirtualKeyboard::~QskVirtualKeyboard()
{
}

void QskVirtualKeyboard::setMode( QskVirtualKeyboard::Mode mode )
{
    m_data->mode = mode;
    polish();

    Q_EMIT modeChanged( m_data->mode );
}

QskVirtualKeyboard::Mode QskVirtualKeyboard::mode() const
{
    return m_data->mode;
}

QSizeF QskVirtualKeyboard::layoutSizeHint(
    Qt::SizeHint which, const QSizeF& constraint ) const
{
    if ( which != Qt::PreferredSize )
        return QSizeF();

    const qreal ratio = qreal( rowCount() ) / columnCount();

    qreal w = constraint.width();
    qreal h = constraint.height();

    if ( h >= 0 )
    {
        const auto padding = innerPadding( Panel, QSizeF( h, h ) );
        const auto dw = padding.left() + padding.right();
        const auto dh = padding.top() + padding.bottom();

        w = ( h - dh ) / ratio + dw;
    }
    else
    {
        if ( w < 0 )
            w = 600;

        const auto padding = innerPadding( Panel, QSizeF( w, w ) );
        const auto dw = padding.left() + padding.right();
        const auto dh = padding.top() + padding.bottom();

        h = ( w - dw ) * ratio + dh;
    }

    return QSizeF( w, h );
}

qreal QskVirtualKeyboard::keyStretch( int key ) const
{
    switch ( key )
    {
        case Qt::Key_Backspace:
        case Qt::Key_Shift:
        case Qt::Key_CapsLock:
            return 1.5;

        case Qt::Key_Space:
            return 3.5;

        case Qt::Key_Return:
        case Qt::Key_Mode_switch:

        // Possibly smaller
        default:
            break;
    }

    return 1.0;
}

bool QskVirtualKeyboard::isKeyVisible( int key ) const
{
    return key != 0;
}

QString QskVirtualKeyboard::textForKey( int key ) const
{
    // Special cases
    switch ( key )
    {
        case Qt::Key_Backspace:
        case Qt::Key_Muhenkan:
            return QChar( 0x232B );

        case Qt::Key_CapsLock:
        case Qt::Key_Kana_Lock:
            return QChar( 0x21E7 );

        case Qt::Key_Shift:
        case Qt::Key_Kana_Shift:
            return QChar( 0x2B06 );

        case Qt::Key_Mode_switch:
            return QChar( 0x2026 );

        case Qt::Key_Return:
        case Qt::Key_Kanji:
            return QChar( 0x23CE );

        case Qt::Key_Left:
            return QChar( 0x2190 );

        case Qt::Key_Right:
            return QChar( 0x2192 );

        case Qt::Key_ApplicationLeft:
            return QChar( 0x2B05 );

        case Qt::Key_ApplicationRight:
            return QChar( 0x27A1 );

        default:
            return QChar( key );
    }
}

QskVirtualKeyboard::KeyType QskVirtualKeyboard::typeForKey( int key ) const
{
    switch( key )
    {
        case Qt::Key_Return:
        case Qt::Key_Enter:
            return EnterType;

        case Qt::Key_Backspace:
            return BackspaceType;

        case Qt::Key_Shift:
        case Qt::Key_CapsLock:
            return CapsSwitchType;

        case Qt::Key_Mode_switch:
            return ModeSwitchType;

        case Qt::Key_Comma:
        case Qt::Key_Period:
            return SpecialCharacterType;

        default:
            return NormalType;
    }
}

void QskVirtualKeyboard::updateLayout() // ### fill keyCodes here
{
    const auto r = layoutRect();
    if ( r.isEmpty() )
        return;

    const auto spacing = spacingHint( Panel );
    const auto totalVSpacing = ( rowCount() - 1 ) * spacing;

    const auto keyHeight = ( r.height() - totalVSpacing ) / rowCount();

    qreal yPos = r.top();

    const auto& page = ( *m_data->currentLayout )[ mode() ];

    for ( int i = 0; i < page.size(); i++ )
    {
        const QVector< int > row = page[ i ];
#if 1
        // there should be a better way
        auto totalHSpacing = -spacing;
        if ( spacing )
        {
            for ( int j = 0; j < row.size(); j++ )
            {
                if ( row[ j ] != 0 )
                    totalHSpacing += spacing;
            }
        }
#endif
        const auto baseKeyWidth = ( r.width() - totalHSpacing ) / rowStretch( row );
        qreal xPos = r.left();

        for ( int j = 0; j < columnCount(); j++ )
        {
            auto button = m_data->keyButtons[ i * columnCount() + j ];

            if( j < row.size() )
            {
                const int key = row[ j ];
                button->setVisible( isKeyVisible( key ) );

                if ( button->isVisible() )
                {
                    const qreal keyWidth = baseKeyWidth * keyStretch( key );

                    const QRectF rect( xPos, yPos, keyWidth, keyHeight );

                    button->setGeometry( rect );
                    button->setAutoRepeat( qskIsAutorepeat( key ) );
                    button->setKey( key );
                    button->setText( textForKey( key ) );

                    const auto type = typeForKey( key );
                    const auto emphasis = emphasisForType( type );
                    button->setEmphasis( emphasis );

                    xPos += keyWidth + spacing;
                }
            }
            else
            {
                button->setVisible( false );
            }
        }

        yPos += keyHeight + spacing;
    }
}

bool QskVirtualKeyboard::hasKey( int keyCode ) const
{
    return m_data->keyCodes.contains( keyCode );
}

int QskVirtualKeyboard::rowCount() const
{
    return m_data->rowCount;
}

void QskVirtualKeyboard::setRowCount( int rowCount )
{
    m_data->rowCount = rowCount;
    ensureButtons();
}

int QskVirtualKeyboard::columnCount() const
{
    return m_data->columnCount;
}

void QskVirtualKeyboard::setColumnCount( int columnCount )
{
    m_data->columnCount = columnCount;
    ensureButtons();
}

QskVirtualKeyboardLayouts QskVirtualKeyboard::layouts() const
{
    return m_data->layouts;
}

void QskVirtualKeyboard::setLayouts( const QskVirtualKeyboardLayouts& layouts )
{
    m_data->layouts = layouts;
}

void QskVirtualKeyboard::ensureButtons()
{
    const int newButtonSize = rowCount() * columnCount();
    const int oldButtonSize = m_data->keyButtons.size();

    if( newButtonSize == oldButtonSize )
        return;

    const auto hints = QGuiApplication::styleHints();

    auto autoRepeatInterval = 1000.0;
#if QT_VERSION >= QT_VERSION_CHECK( 6, 5, 0 )
        autoRepeatInterval /= hints->keyboardAutoRepeatRateF();
#else
        autoRepeatInterval /= hints->keyboardAutoRepeatRate();
#endif

    m_data->keyButtons.reserve( rowCount() * columnCount() );

    for( int i = 0; i < rowCount(); i++ )
    {
        for( int j = 0; j < columnCount(); j++ )
        {
            const int index = i * columnCount() + j;

            if( index >= m_data->keyButtons.size() )
            {
                auto button = new Button( this );
                button->installEventFilter( this );

                button->setAutoRepeat( false );
                button->setAutoRepeatDelay( 500 );
                button->setAutoRepeatInterval( autoRepeatInterval );

                connect( button, &QskPushButton::pressed,
                    this, &QskVirtualKeyboard::buttonPressed );

                m_data->keyButtons += button;
            }
        }
    }

    while( m_data->keyButtons.size() > newButtonSize )
    {
        auto* button = m_data->keyButtons.takeLast();
        button->deleteLater();
    }
}

void QskVirtualKeyboard::buttonPressed()
{
    const auto button = static_cast< const Button* >( sender() );
    if ( button == nullptr )
        return;

    const int key = button->key();

    // Mode-switching keys
    switch ( key )
    {
        case Qt::Key_CapsLock:
        case Qt::Key_Kana_Lock:
        {
            setMode( UppercaseMode ); // Lock caps
            break;
        }

        case Qt::Key_Shift:
        case Qt::Key_Kana_Shift:
        {
            setMode( LowercaseMode ); // Unlock caps
            break;
        }

        case Qt::Key_Mode_switch: // Cycle through modes, but skip caps
        {
            setMode( static_cast< QskVirtualKeyboard::Mode >(
                m_data->mode ? ( ( m_data->mode + 1 ) % QskVirtualKeyboard::ModeCount )
                             : SpecialCharacterMode ) );

            break;
        }
        default:
        {
            Q_EMIT keySelected( key );
        }
    }
}

void QskVirtualKeyboard::updateKeyCodes()
{
    m_data->keyCodes = {};
    m_data->keyCodes.reserve( rowCount() * columnCount() );

    const auto layout = *m_data->currentLayout;

    for ( int mode = 0; mode < layout.size(); mode++ )
    {
        const auto& page = layout[ mode ];

        for ( int i = 0; i < page.size(); i++ )
        {
            const auto& row = page[ i ];

            for ( int j = 0; j < row.size(); j++ )
            {
                m_data->keyCodes += row[ j ];
            }
        }
    }
}

qreal QskVirtualKeyboard::rowStretch( const QVector< int >& row )
{
    qreal stretch = 0;

    for ( const int& key : row )
    {
        if ( !key )
        {
            continue;
        }

        stretch += keyStretch( key );
    }

    if ( stretch == 0.0 )
    {
        stretch = columnCount();
    }

    return stretch;
}

void QskVirtualKeyboard::updateLocale( const QLocale& locale )
{
    const QskVirtualKeyboardLayouts::Layout* newLayout = nullptr;

    switch ( locale.language() )
    {
        case QLocale::Bulgarian:
            newLayout = &m_data->layouts.bg;
            break;

        case QLocale::Czech:
            newLayout = &m_data->layouts.cs;
            break;

        case QLocale::German:
            newLayout = &m_data->layouts.de;
            break;

        case QLocale::Danish:
            newLayout = &m_data->layouts.da;
            break;

        case QLocale::Greek:
            newLayout = &m_data->layouts.el;
            break;

        case QLocale::English:
        {
            const auto territory =
#if QT_VERSION >= QT_VERSION_CHECK( 6, 2, 0 )
                locale.territory();
#else
                locale.country();
#endif

            switch ( territory )
            {
                case QLocale::Canada:
                case QLocale::UnitedStates:
                case QLocale::UnitedStatesMinorOutlyingIslands:
                case QLocale::UnitedStatesVirginIslands:
                    newLayout = &m_data->layouts.en_US;
                    break;

                default:
                    newLayout = &m_data->layouts.en_GB;
                    break;
            }

            break;
        }

        case QLocale::Spanish:
            newLayout = &m_data->layouts.es;
            break;

        case QLocale::Finnish:
            newLayout = &m_data->layouts.fi;
            break;

        case QLocale::French:
            newLayout = &m_data->layouts.fr;
            break;

        case QLocale::Hungarian:
            newLayout = &m_data->layouts.hu;
            break;

        case QLocale::Italian:
            newLayout = &m_data->layouts.it;
            break;

        case QLocale::Japanese:
            newLayout = &m_data->layouts.ja;
            break;

        case QLocale::Latvian:
            newLayout = &m_data->layouts.lv;
            break;

        case QLocale::Lithuanian:
            newLayout = &m_data->layouts.lt;
            break;

        case QLocale::Dutch:
            newLayout = &m_data->layouts.nl;
            break;

        case QLocale::Portuguese:
            newLayout = &m_data->layouts.pt;
            break;

        case QLocale::Romanian:
            newLayout = &m_data->layouts.ro;
            break;

        case QLocale::Russian:
            newLayout = &m_data->layouts.ru;
            break;

        case QLocale::Slovenian:
            newLayout = &m_data->layouts.sl;
            break;

        case QLocale::Slovak:
            newLayout = &m_data->layouts.sk;
            break;

        case QLocale::Turkish:
            newLayout = &m_data->layouts.tr;
            break;

        case QLocale::Chinese:
            newLayout = &m_data->layouts.zh;
            break;
#if 1
        case QLocale::C:
            newLayout = &m_data->layouts.en_US;
            break;
#endif
        default:
            qWarning() << "QskVirtualKeyboard: unsupported locale:" << locale;
            newLayout = &m_data->layouts.en_US;
    }

    if ( newLayout != m_data->currentLayout )
    {
        m_data->currentLayout = newLayout;
        updateKeyCodes();

        setMode( LowercaseMode );
        polish();
        Q_EMIT keyboardLayoutChanged();
    }
}

QskPushButton::Emphasis QskVirtualKeyboard::emphasisForType( KeyType type )
{
    switch( type )
    {
        case EnterType:
            return QskPushButton::VeryHighEmphasis;

        case BackspaceType:
        case CapsSwitchType:
            return QskPushButton::HighEmphasis;

        case ModeSwitchType:
            return QskPushButton::LowEmphasis;

        case SpecialCharacterType:
            return QskPushButton::VeryLowEmphasis;

        default:
            return QskPushButton::NoEmphasis;
    }
}

#include "moc_QskVirtualKeyboard.cpp"
