/*
Copyright (c) 2006-2009, Tom Thielicke IT Solutions

SPDX-License-Identifier: GPL-2.0-only
*/

/****************************************************************
**
** Implementation of the TrainingWidget class
** File name: trainingwidget.cpp
**
****************************************************************/

#include <QApplication>
#include <QCoreApplication>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QSettings>
#include <QSqlQuery>
#include <QVBoxLayout>
#include <QVariant>

#include "def/defines.h"
#include "errormessage.h"
#include "trainingwidget.h"

TrainingWidget::TrainingWidget(
    int lesson, int type, QString name, QWidget* parent)
    : QWidget(parent)
    , tickerBoard(new TickerBoard(this))
    , currentChar(' ')
    , currentLesson(lesson)
    , currentType(type)
    , currentName(name)
    , currentStrokes(0)
    , currentChars(0)
    , currentErrors(0)
    , currentSeconds(0)
    , counterToNewLine(0)
    , startTime(QDateTime::currentDateTime())
    , isStarted(false)
    , isPaused(false)
    , errorCorrectFlag(false)
    , oneErrorFlag(false)
    , timer(new QTimer(this))
    , metronomeTimer(new QTimer(this))
{
    // Init sound file
    bells.setSource(
        QUrl::fromLocalFile(t10::getDataDir() + QString("error.wav")));
    metronomeSound.setSource(
        QUrl::fromLocalFile(t10::getDataDir() + QString("metronome.wav")));

    // Create all type trainer objects
    // 1. Keyboard (set focus!)
    if ((currentLesson % 100) >= t10::numpad_lesson_start && currentType == 0) {
        numPad = new NumPad(this);
    } else {
        keyBoard = new KeyBoard(this);
    }

    readSettings();

    // 2. Statusbar
    statusBar = new StatusBar(this);
    trainingSql = new TrainingSql(replaceSetting, regexpSetting, layoutSetting);
    // Create Buttons "Cancel" and "Pause"
    createButtons();
    // Create layout of widgets
    createLayout();
    // Create connections
    createConnections();

    keyboardSql = new KeyboardSql(opSystem);

    // Create lesson text
    // createLesson();
    if (!showHelpers) {
        if ((currentLesson % 100) >= t10::numpad_lesson_start
            && currentType == 0) {
            numPad->setVisible(false);
        } else {
            keyBoard->setVisible(false);
        }
        // statusBar->setVisible(false);
        parent->setMinimumSize(t10::app_width_small, t10::app_height_small);
        if (!parent->isMaximized()) {
            parent->resize(t10::app_width_small, t10::app_height_small);
        }
    }
    tickerBoard->setFocus();
    // Start all
    startSession();
}

void TrainingWidget::createButtons()
{
    // Pause button
    buttonPause = new QPushButton(tr("&Pause"));
    buttonPause->setEnabled(false);
    buttonPause->setFocusPolicy(Qt::NoFocus);
#ifdef APP_MAC
    buttonPause->setShortcut(Qt::ALT + Qt::Key_P);
#endif
    // Cancel button
    buttonCancel = new QPushButton(tr("&Cancel"));
    buttonCancel->setFocusPolicy(Qt::NoFocus);
#ifdef APP_MAC
    buttonCancel->setShortcut(Qt::ALT + Qt::Key_B);
#endif
    // Help button
    buttonHelp = new QPushButton(tr("&Help"));
    buttonHelp->setFocusPolicy(Qt::NoFocus);
}

void TrainingWidget::createLayout()
{
    // Bottom layout
    QHBoxLayout* bottomLayout = new QHBoxLayout;
    bottomLayout->addStretch(1);
    bottomLayout->addWidget(buttonCancel);
    bottomLayout->addSpacing(10);
    bottomLayout->addWidget(buttonHelp);
    bottomLayout->addWidget(buttonPause);
    // Ticker layout horizontal
    QHBoxLayout* tickerboardLayout = new QHBoxLayout;
    tickerboardLayout->addStretch(1);
    tickerboardLayout->addWidget(tickerBoard);
    tickerboardLayout->addStretch(1);
    // Keyboard layout horizontal
    QHBoxLayout* keyboardLayout = new QHBoxLayout;
    keyboardLayout->addStretch(1);
    if ((currentLesson % 100) >= t10::numpad_lesson_start && currentType == 0) {
        keyboardLayout->addWidget(numPad);
    } else {
        keyboardLayout->addWidget(keyBoard);
    }
    keyboardLayout->addStretch(1);
    // Statusbar layout horizontal
    QHBoxLayout* statusLayout = new QHBoxLayout;
    statusLayout->addStretch(1);
    statusLayout->addWidget(statusBar);
    statusLayout->addStretch(1);
    // Full layout of all widgets vertical
    QVBoxLayout* mainLayout = new QVBoxLayout;
    mainLayout->addStretch(1);
    mainLayout->addLayout(tickerboardLayout);
    mainLayout->addLayout(keyboardLayout);
    mainLayout->addLayout(statusLayout);
    mainLayout->addStretch(1);
    mainLayout->addSpacing(10);
    mainLayout->addLayout(bottomLayout);
    mainLayout->setSpacing(15);
    // Pass layout to parent widget (this)
    this->setLayout(mainLayout);
}

void TrainingWidget::createConnections()
{
    connect(timer, &QTimer::timeout, this, &TrainingWidget::secondsUpdate);
    connect(metronomeTimer, &QTimer::timeout, this,
        &TrainingWidget::metronomeOutput);
    // Incoming connections
    // Incoming connection from TickerBoard object
    connect(
        tickerBoard, &TickerBoard::charChanged, this, &TrainingWidget::setChar);
    connect(tickerBoard, &TickerBoard::updateRequired, this,
        &TrainingWidget::updateLesson);
    // Incoming connection from KeyBoard object
    connect(
        tickerBoard, &TickerBoard::keyPressed, this, &TrainingWidget::setKey);
    // connect(tickerBoard, &TickerBoard::isReady, this,
    // &TrainingWidget::exitTraining);
    // Button connections
    connect(buttonPause, &QPushButton::clicked, this,
        &TrainingWidget::pauseSession);
    connect(buttonPause, &QPushButton::clicked, this,
        [this]() { tickerBoard->pauseTicker(); });
    connect(buttonCancel, &QPushButton::clicked, this,
        &TrainingWidget::cancelSession);
    connect(
        buttonHelp, &QPushButton::clicked, this, &TrainingWidget::pauseSession);
    connect(buttonHelp, &QPushButton::clicked, this,
        [this]() { tickerBoard->pauseTicker(); });
    connect(buttonHelp, &QPushButton::clicked, this, &TrainingWidget::showHelp);
    if ((currentLesson % 100) >= t10::numpad_lesson_start && currentType == 0) {
        // Verbindung zwischen Laufschrift- und Tastaturklasse
        connect(tickerBoard, &TickerBoard::isReady, numPad, &NumPad::stopBoard);
        connect(numPad, &NumPad::statusRefreshed, this,
            &TrainingWidget::updateStatusText);
        connect(
            buttonPause, &QPushButton::clicked, numPad, &NumPad::pauseBoard);
        connect(buttonHelp, &QPushButton::clicked, numPad, &NumPad::pauseBoard);
    } else {
        // Verbindung zwischen Laufschrift- und Tastaturklasse
        connect(
            tickerBoard, &TickerBoard::isReady, keyBoard, &KeyBoard::stopBoard);
        connect(keyBoard, &KeyBoard::statusRefreshed, this,
            &TrainingWidget::updateStatusText);
        connect(buttonPause, &QPushButton::clicked, keyBoard,
            &KeyBoard::pauseBoard);
        connect(
            buttonHelp, &QPushButton::clicked, keyBoard, &KeyBoard::pauseBoard);
    }
}

void TrainingWidget::startSession()
{
    timer->start(1000);
    if (metronomeClock != 0) {
        metronomeTimer->start(metronomeClock);
    }
    createLesson();
    tickerBoard->pauseTicker(tr("Press space bar to start"));
    statusBar->setCenterText(tr("Take Home Row position"));
}

void TrainingWidget::pauseSession()
{
    buttonPause->setEnabled(false);
    isPaused = true;
}

void TrainingWidget::cancelSession()
{
    isPaused = true;
    // Ask only is lesson is started and if anything was typed at all
    if (isStarted && currentStrokes > 0) {
        // Ask if user wants to quit really
        switch (QMessageBox::question(this, t10::app_name,
            tr("Do you really want to exit the lesson early?"))) {
        case QMessageBox::Yes:
            // User wants to quit
            break;
        case QMessageBox::No:
            // User canceled the message
            // -> go back to the training
            isPaused = false;
            return;
        default:
            break;
        }
        // Ask if user want to save data
        switch (QMessageBox::question(
            this, t10::app_name, tr("Do you want to save your results?"))) {
        case QMessageBox::Yes:
            // Save and exit
            exitTraining();
            return;
        case QMessageBox::No:
            // Don't save and exit (cancel like on the beginning)
            // -> go back to start widget
            emit lessonCanceled();
            return;
        default:
            break;
        }
    } else {
        // Nothing done yet
        // -> go back to start widget
        emit lessonCanceled();
    }
}

void TrainingWidget::createLesson()
{
    QString lessonString;
    if ((lessonString = trainingSql->createLesson(currentLesson, currentType,
             lessonUnit, useIntelligence, useEszett, useUmlaut))
        == "") {
        // No lesson created
        // -> error message
        ErrorMessage* errorMessage = new ErrorMessage(this);
        errorMessage->showMessage(Error::lessons_creation,
            ErrorMessage::Type::Critical, ErrorMessage::Cancel::Operation);
        return;
    }
    switch (currentType) {
    case 0:
        // Training lesson
        counterToNewLine = lessonString.length() + 1;
        if ((currentLesson % 100) < t10::border_lesson_is_sentence) {
            lessonString.append(" ");
        } else {
            lessonString.append(t10::token_new_line);
        }
        break;
    case 1:
    case 2:
        // Open or own lesson
        if (limitType != 2) {
            // Exit lesson or unit with newline
            counterToNewLine = lessonString.length() + 1;
            if (lessonUnit == 1) {
                lessonString.append(" ");
            } else {
                lessonString.append(t10::token_new_line);
            }
        } else {
            // Hole lesson
            counterToNewLine = lessonString.length() - 2;
        }
        break;
    }
    counterChars = lessonString.length();
    tickerBoard->setTicker(lessonString);
}

void TrainingWidget::updateLesson()
{
    QString lessonString;

    if (limitType == 2) {
        return;
    }
    if ((lessonString = trainingSql->updateLesson(
             currentLesson, currentType, useIntelligence, useEszett, useUmlaut))
        == "") {
        // No update lesson created
        // -> error message
        ErrorMessage* errorMessage = new ErrorMessage(this);
        errorMessage->showMessage(Error::lessons_update,
            ErrorMessage::Type::Critical, ErrorMessage::Cancel::Operation);
        return;
    }
    counterToNewLine += lessonString.length();
    if ((currentType == 0
            && (currentLesson % 100) < t10::border_lesson_is_sentence
            && counterToNewLine > t10::num_token_until_new_line)
        || (currentType == 0
            && (currentLesson % 100) >= t10::border_lesson_is_sentence)
        || (currentType != 0
            && (lessonUnit == 0
                || (lessonUnit == 1
                    && counterToNewLine > t10::num_token_until_new_line)))) {
        lessonString.append(t10::token_new_line);
        counterToNewLine = 0;
    } else {
        counterToNewLine++;
        lessonString.append(" ");
    }
    counterChars += lessonString.length();
    tickerBoard->extendTicker(lessonString);
}

// Slot: Aktuellen Buchstaben setzen
void TrainingWidget::setChar(QChar newchar)
{
    currentChars++;
    currentChar = newchar;
    if ((currentLesson % 100) >= t10::numpad_lesson_start && currentType == 0) {
        numPad->setKey(currentChar);
    } else {
        keyBoard->setKey(currentChar);
    }
    if (!trainingSql->updateUsertable(currentChar, "user_char_occur_num")) {
        ErrorMessage* errorMessage = new ErrorMessage(this);
        errorMessage->showMessage(Error::user_errors_refresh,
            ErrorMessage::Type::Critical, ErrorMessage::Cancel::Operation);
    }
}

// Slot: Aktuellen Buchstaben setzen
void TrainingWidget::setKey(QChar key)
{
    if (isStarted && !isPaused) {
        if (errorCorrectFlag) {
            // Ruecklauftaste
            if (key.unicode() == 8) {
                errorCorrectFlag = false;
                if ((currentLesson % 100) >= t10::numpad_lesson_start
                    && currentType == 0) {
                    numPad->setKey(currentChar);
                } else {
                    keyBoard->setKey(currentChar);
                }
                tickerBoard->clearErrorSelection();
                oneErrorFlag = false;
            }
        } else {
            // Check if correct key was pressed OR
            // key was enter and a line break was required
            // (char and unicode then are different)
            if (key == currentChar
                || ((key.unicode() == 13 || key.unicode() == 3)
                    && currentChar == t10::token_new_line)
                || (key.unicode() == 9 && currentChar == t10::token_tab)) {
                // currentChar.unicode() == 182)) {
                // Correct key was pressed
                oneErrorFlag = false;
                currentStrokes++;
                tickerBoard->getNewChar();

                charList << key;
                mistakeList << 0;
            } else {
                // Wrong key was pressed
                if (!oneErrorFlag) {
                    if (beepOnError) {
                        bells.play();
                    }
                    currentErrors++;
                    tickerBoard->setErrorSelection();
                    update();

                    if (key.unicode() == 13 || key.unicode() == 3) {
                        charList << t10::token_new_line;
                    } else {
                        if (key.unicode() == 9) {
                            charList << t10::token_tab;
                        } else {
                            charList << key;
                        }
                    }
                    mistakeList << 1;

                    if (!trainingSql->updateUsertable(
                            currentChar, "user_char_target_errornum")) {
                        // Error message
                        ErrorMessage* errorMessage = new ErrorMessage(this);
                        errorMessage->showMessage(Error::user_errors_refresh,
                            ErrorMessage::Type::Critical,
                            ErrorMessage::Cancel::Operation);
                    }
                    if (!trainingSql->updateUsertable(
                            key, "user_char_mistake_errornum")) {
                        // Error message
                        ErrorMessage* errorMessage = new ErrorMessage(this);
                        errorMessage->showMessage(Error::user_errors_refresh,
                            ErrorMessage::Type::Critical,
                            ErrorMessage::Cancel::Operation);
                    }
                    oneErrorFlag = true;
                }
                if (!stopOnError) {
                    oneErrorFlag = false;
                    currentStrokes++;
                    tickerBoard->getNewChar();
                }
                // statusBar->setCenterText(QString::number(key.unicode()));
                if (correctOnError) {
                    errorCorrectFlag = true;
                    if ((currentLesson % 100) >= t10::numpad_lesson_start
                        && currentType == 0) {
                        numPad->setKey(t10::token_backspace);
                    } else {
                        keyBoard->setKey(t10::token_backspace);
                    }
                    // statusBar->setCenterText(tr("Kleiner Finger "
                    //	"rechts - Ruecklauftaste!"));
                }
                // update();
            }
        }
        updateStatusValues();
    } else {
        if (key == ' ') {
            bool wasPaused = isPaused;
            isStarted = true;
            isPaused = false;
            buttonPause->setEnabled(true);
            buttonCancel->setText(tr("E&xit Lesson early"));
            // Start board first, cause starting the ticker
            // emits a signal which need a started keyBoard
            if ((currentLesson % 100) >= t10::numpad_lesson_start
                && currentType == 0) {
                numPad->startBoard();
            } else {
                keyBoard->startBoard();
            }
            tickerBoard->startTicker(wasPaused);
            return;
        }
    }
}

void TrainingWidget::secondsUpdate()
{
    if (isStarted && !isPaused) {
        currentSeconds++;
        if ((limitType == 0 && limitValue <= (currentSeconds / 60))
            || (limitType == 1 && limitValue <= currentChars)
            || (limitType == 2 && (counterChars + 1) <= currentChars)) {
            exitTraining();
        } else {
            updateStatusValues();
        }
    }
}

void TrainingWidget::metronomeOutput()
{
    if (isStarted && !isPaused) {
        metronomeSound.play();
    }
}

void TrainingWidget::updateStatusValues()
{
    // CPM
    auto minutes = static_cast<double>(currentSeconds) / 60.0;
    double strokesPerMinute = 0.;
    if (minutes != 0) {
        strokesPerMinute = static_cast<double>(currentStrokes) / minutes;
    }
    // Time
    int timeMinutes = currentSeconds / 60;
    int timeSeconds = currentSeconds % 60;
    QString time = QString::number(timeMinutes) + ":";
    if (timeSeconds < 10) {
        time.append("0" + QString::number(timeSeconds));
    } else {
        time.append(QString::number(timeSeconds));
    }
    statusBar->setLeftLeftText(tr("Errors: ") + QString::number(currentErrors));
    statusBar->setLeftText(
        tr("Cpm: ") + QString::number(static_cast<uint32_t>(strokesPerMinute)));
    statusBar->setRightText(tr("Time: ") + time);
    statusBar->setRightRightText(
        tr("Characters: ") + QString::number(currentChars));
}

void TrainingWidget::updateStatusText(QString statustext)
{
    if (!showStatusInformation) {
        statustext = "";
    }
    statusBar->setCenterText(statustext);
}

void TrainingWidget::exitTraining()
{
    QVariant lastRowId;
    lastRowId = trainingSql->saveLesson(currentLesson, currentSeconds,
        currentChars - 1, currentStrokes, currentErrors, startTime, currentType,
        currentName);
    if (!lastRowId.isValid()) {
        // No lesson created
        // -> error message
        ErrorMessage* errorMessage = new ErrorMessage(this);
        errorMessage->showMessage(Error::user_lessons_refresh,
            ErrorMessage::Type::Critical, ErrorMessage::Cancel::Operation);
        lastRowId = 0;
    }
    emit lessonReady(lastRowId.toInt(), currentType, charList, mistakeList);
}

void TrainingWidget::showHelp()
{
    helpBrowser = new HelpBrowser("training.html", nullptr);
    helpBrowser->show();
}

void TrainingWidget::readSettings()
{
// Read settings of the startwiget
// (uses the default constructor of QSettings, passing
// the application and company name see main function)
#if APP_PORTABLE
    QSettings settings(
        QCoreApplication::applicationDirPath() + "/portable/settings.ini",
        QSettings::IniFormat);
#else
    QSettings settings;
#endif
    settings.beginGroup("main");
    if (settings.value("language_layout", t10::app_std_language_layout)
            .toString()
            .right(3)
        == "win") {
        opSystem = "win";
    } else {
        opSystem = "mac";
    }
    useEszett = true;
    if (settings.value("language_layout", t10::app_std_language_layout)
            .toString()
            .left(2)
        == "ch") {
        useEszett = false;
    }
    useUmlaut = true;
    if (settings.value("language_layout", t10::app_std_language_layout)
            .toString()
            .left(2)
        == "us") {
        useUmlaut = false;
    }
    layoutSetting
        = settings.value("language_layout", t10::app_std_language_layout)
              .toString();
    replaceSetting = settings.value("layout_replace", "NULL").toString();
    regexpSetting = settings.value("layout_regexp", "NULL").toString();

    settings.endGroup();

    settings.beginGroup("duration");
    if (settings.value("radio_time", true).toBool()) {
        // Time limit selected
        limitType = 0;
        limitValue
            = settings.value("spin_time", t10::lesson_timelen_standard).toInt();
    } else {
        if (settings.value("radio_token", true).toBool()) {
            // Token limit selected
            limitType = 1;
            limitValue
                = settings.value("spin_token", t10::lesson_tokenlen_standard)
                      .toInt();
        } else {
            // Lesson limit selected
            limitType = 2;
            limitValue = 0;
        }
    }
    settings.endGroup();
    settings.beginGroup("error");
    stopOnError = settings.value("check_stop", true).toBool();
    correctOnError = settings.value("check_correct", false).toBool();
    beepOnError = settings.value("check_beep", false).toBool();
    settings.endGroup();
    settings.beginGroup("support");
    showHelpers = settings.value("check_helpers", true).toBool();
    showStatusInformation = settings.value("check_status", true).toBool();
    settings.endGroup();
    settings.beginGroup("sound");
    if (settings.value("check_metronome", false).toBool()) {
        metronomeClock
            = settings.value("spin_metronome", t10::metronom_standard).toInt();
        metronomeClock = 60000 / metronomeClock;
    }
    settings.endGroup();

    if (currentType != 0) {
        settings.beginGroup("intelligence");
        useIntelligence = settings.value("check_intelligence", false).toBool();
        settings.endGroup();
    } else {
        useIntelligence = true;
    }
    if (currentType != 0) {
        lessonUnit = trainingSql->getLessonUnit(currentLesson, currentType);
    } else {
        lessonUnit = 0;
    }
}
