/***************************************************************************
 *   ktouchtrainer.cpp                                                     *
 *   -----------------                                                     *
 *   Copyright (C) 2000 by Håvard Frøiland, 2006 by Andreas Nicolai        *
 *   ghorwin@users.sourceforge.net                                         *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 ***************************************************************************/

#include "ktouchtrainer.h"
#include "ktouchtrainer.moc"

#include <tqlcdnumber.h>
#include <tqfile.h>

#include <kdebug.h>
#include <kpushbutton.h>
#include <tdelocale.h>
#include <tdestandarddirs.h>
#include <kaudioplayer.h>
#include <tqmessagebox.h>

#include "ktouch.h"
#include "ktouchstatus.h"
#include "ktouchslideline.h"
#include "ktouchkeyboardwidget.h"
#include "ktouchlecture.h"
#include "ktouchdefaults.h"
#include "prefs.h"

KTouchTrainer::KTouchTrainer(KTouchStatus *status, KTouchSlideLine *slideLine, KTouchKeyboardWidget *keyboard, KTouchLecture *lecture)
  : TQObject(),
    m_trainingTimer(new TQTimer),
    m_statusWidget(status),
    m_slideLineWidget(slideLine),
    m_keyboardWidget(keyboard),
    m_lecture(lecture)
{
    m_level = 0;              // level 1, but we're storing it zero based
    m_line  = 0;
	m_wordsInCurrentLine = 0;
    m_trainingPaused=true;    // we start in pause mode
    m_teacherText=m_lecture->level(0).line(0);
    m_studentText="";
	
	// reset statistics
    m_levelStats.clear();
	m_sessionStats.clear();

	/// \todo preload sounds and improve sound playback system
    m_levelUpSound = TDEGlobal::dirs()->findResource("appdata","up.wav");
    m_levelDownSound = TDEGlobal::dirs()->findResource("appdata","down.wav");
    m_typeWriterSound = TDEGlobal::dirs()->findResource("appdata","typewriter.wav");

    connect(m_statusWidget->levelUpBtn, TQ_SIGNAL(clicked()), this, TQ_SLOT(levelUp()) );
    connect(m_statusWidget->levelDownBtn, TQ_SIGNAL(clicked()), this, TQ_SLOT(levelDown()) );
    connect(m_trainingTimer, TQ_SIGNAL(timeout()), this, TQ_SLOT(timerTick()) );
}
// ----------------------------------------------------------------------------

KTouchTrainer::~KTouchTrainer() {
    delete m_trainingTimer;
}
// ----------------------------------------------------------------------------

void KTouchTrainer::gotoFirstLine() {
    m_statusWidget->setNewChars( m_lecture->level(m_level).newChars() );
    m_line=0;
    newLine();
}
// ----------------------------------------------------------------------------

void KTouchTrainer::keyPressed(TQChar key) {
	// NOTE : In this function we need to distinguish between left and right
	//        typing. Use the config setting Prefs::right2LeftTyping() for that.

	if (m_trainingPaused)  continueTraining();
    if (m_teacherText==m_studentText) {
        // if already at end of line, don't add more chars
        /// \todo Flash the line when line complete
        if (Prefs::beepOnError())   TQApplication::beep();
        return;
    }
	// remember length of student text without added character
    unsigned int old_student_text_len = m_studentText.length();
    // compose new student text depending in typing direction
    TQString new_student_text = m_studentText;
    if (Prefs::right2LeftTyping())
        new_student_text = key + new_student_text;
    else
        new_student_text += key;

    // don´t allow excessive amounts of characters per line
	if (!m_slideLineWidget->canAddCharacter(new_student_text)) {
        if (Prefs::beepOnError())   TQApplication::beep();
        return;
	}
    // store the new student text
    m_studentText = new_student_text;
    // we need to find out, if the key was correct or not
    if (studentLineCorrect())
		statsAddCorrectChar(key);	// ok, all student text is correct
    else {
        // nope, the key was wrong : beep !!!
        if (Prefs::beepOnError())  TQApplication::beep();
        // check if the key is the first wrong key that was mistyped. Only then add it
		// to the wrong char statistics.
        if (Prefs::right2LeftTyping()) {
			if (m_teacherText.right(old_student_text_len)==m_studentText.right(old_student_text_len) &&
			    m_teacherText.length() > old_student_text_len)
			{
				// add the key the student ought to press to the wrong character stats
				int next_key_index = m_teacherText.length() - old_student_text_len;
//				kdDebug() << "Wrong key = " << m_teacherText[next_key_index] << endl;
				statsAddWrongChar( m_teacherText[next_key_index] );
			}
        }
        else {
			if (m_teacherText.left(old_student_text_len)==m_studentText.left(old_student_text_len) &&
				m_teacherText.length() > old_student_text_len)
			{
				// add the key the student ought to press to the wrong character stats
				int next_key_index = old_student_text_len;
				statsAddWrongChar( m_teacherText[next_key_index] );
			}
		}
		/// \todo 	Implement option whether subsequent mistyped keys should be remembered as missed
		///			keys as well.
	}
    updateWidgets(); // update all the other widgets (keyboard widget, status widget and statusbar)
}
// ----------------------------------------------------------------------------

void KTouchTrainer::backspacePressed() {
	if (m_trainingPaused)  continueTraining();
	/// \todo Implement the "remove space character = remove word count" feature

    unsigned int len = m_studentText.length();
    if (len) {
        if (m_teacherText.left(len)==m_studentText && m_teacherText.length()>=len) {
            // we are removing a correctly typed char
			statsRemoveCorrectChar(m_studentText[len-1]);
        }
        m_studentText = m_studentText.left(--len);
        updateWidgets(); // update all the widgets and the word count
        if (m_teacherText.left(len)==m_studentText)
            m_keyboardWidget->newKey(m_teacherText[len]);
        else
            m_keyboardWidget->newKey(TQChar(8));
    }
    else {
        /// \todo Flash line when Backspace on empty line
        TQApplication::beep();
	}
}
// ----------------------------------------------------------------------------

void KTouchTrainer::enterPressed() {
	if (m_trainingPaused)  continueTraining();
    if (m_studentText!=m_teacherText) {
        TQApplication::beep();
        return;
    };

	/* 
	// NOTE : auto level change inside level was removed due to popular request
	
    if (Prefs::autoLevelChange()) {
        // if level increase criterion was fulfilled, increase line counter
        if (Prefs::upCorrectLimit() <= m_session.correctness()*100 &&
            Prefs::upSpeedLimit() <= m_session.charSpeed())
        {
            m_decLinesCount=0;
            ++m_incLinesCount;
        }
        else  if (Prefs::downCorrectLimit() > m_session.correctness()*100 ||
                  Prefs::downSpeedLimit() > m_session.charSpeed())
        {
            m_incLinesCount=0;
            ++m_decLinesCount;
        };
        // Automatic level change after a number of lines can happen, if you fulfilled the
        // requirements in the last 5 lines.
        if (m_incLinesCount>=2) {
            levelUp();
            return;
        }
        if (m_decLinesCount>=2 && m_level!=0) {
            levelDown();
            return;
        };
    };
	*/


	// Check if we are in the last line
    if (m_line+1 >= m_lecture->level(m_level).count()) {
        if (Prefs::autoLevelChange()) {
            // adjust level if limits exceeded
            if (Prefs::upCorrectLimit() <= m_levelStats.correctness()*100 &&
				Prefs::upSpeedLimit() <= m_levelStats.charSpeed())
            {
				/// \todo	Test if last level is done and show message, stop training, show statistics etc.
                levelUp();	// level change takes care of updating word count
                return;
            }
			else  if (Prefs::downCorrectLimit() > m_levelStats.correctness()*100 ||
				Prefs::downSpeedLimit() > m_levelStats.charSpeed()) 
            {
                levelDown();	// level change takes care of updating word count
                return;
            }
        }
		// we have to store the word count before continuing in the first line
		updateWordCount();
		m_levelStats.m_words += m_wordsInCurrentLine;
		m_sessionStats.m_words += m_wordsInCurrentLine;
		gotoFirstLine();  // restart in the new/current level
    }
    else {
		// store the word count
		updateWordCount();
		m_levelStats.m_words += m_wordsInCurrentLine;
		m_sessionStats.m_words += m_wordsInCurrentLine;
    	++m_line;
		newLine(); // otherwise show next line
	}
}
// ----------------------------------------------------------------------------

void KTouchTrainer::updateWidgets() {
    // update status widget
    m_statusWidget->updateStatus(m_level, m_levelStats.correctness());
    // update slide line widget
    m_slideLineWidget->setStudentText(m_studentText);
    // update keyboard widget -> show next to be pressed char.
    // we have to find out first whether the student text is correct or not.
    if (studentLineCorrect()) {
        // ok, all student text is correct
        if (m_teacherText.length()==m_studentText.length())
            m_keyboardWidget->newKey(TQChar(13));        // we have reached the end of the line
        else
            m_keyboardWidget->newKey(m_teacherText[m_studentText.length()]);
    }
    else {
        m_keyboardWidget->newKey(TQChar(8)); // wrong key, user must now press backspace
    }
	updateWordCount();	// here we first update the word count 
	updateStatusBar();	// and then the status bar
}
// ----------------------------------------------------------------------------

void KTouchTrainer::startTraining(bool keepLevel) {
	// Here we start a new training session.
	
	// keep the current level if flag is set
	if (!keepLevel)
		m_level=0;
	// reset the level and session statistics
	m_levelStats.clear();
	m_sessionStats.clear();
	// transfer level to level statistics
	m_levelStats.m_levelNum = m_level;
	// remember level in session stats
	m_sessionStats.m_levelNums.insert(m_level);
	// go to first line in level (also resets word count)
	gotoFirstLine();
	updateStatusBarMessage(i18n("Starting training session: Waiting for first keypress...") );
	updateStatusBar();
	m_statusWidget->updateStatus(m_level, 1);
	m_statusWidget->speedLCD->display( 0 );
	m_trainingPaused=true;		// Go into "Pause" mode
	m_trainingTimer->stop();    // Training timer will be started on first keypress.
	m_slideLineWidget->setCursorTimerEnabled(true); // Curser will blink
	updateLevelChangeButtons();
}
// ----------------------------------------------------------------------------

// Pauses the current training session.
// This function is called from class KTouch (when the Pause action is executed).
void KTouchTrainer::pauseTraining() {
	m_trainingPaused=true;
	m_trainingTimer->stop();
	m_slideLineWidget->setCursorTimerEnabled(false);
	m_statusWidget->updateStatus(m_level, m_levelStats.correctness());
	m_statusWidget->speedLCD->display( m_levelStats.charSpeed() );
	updateStatusBarMessage(i18n("Training session paused. Training continues on next keypress...") );
	updateStatusBar();
}
// ----------------------------------------------------------------------------

// Continues the current training session.
// This function is called from class KTouch when a user presses a normal key 
// while the training is in pause mode.
void KTouchTrainer::continueTraining() {
	m_trainingPaused=false;
	m_slideLineWidget->setCursorTimerEnabled(true);
	m_statusWidget->updateStatus(m_level, m_levelStats.correctness() );
	m_statusWidget->speedLCD->display( m_levelStats.charSpeed() );
	updateStatusBarMessage(i18n("Training session! The time is running...") );
	updateStatusBar();
	m_trainingTimer->start(LCD_UPDATE_INTERVAL);    // start the timer
}
// ----------------------------------------------------------------------------

void KTouchTrainer::storeTrainingStatistics() {
	// at first get a reference to the statistics data of the current lecture
	KTouchLectureStats& data = KTouchPtr->getCurrentLectureStats();
	// update word count
	updateWordCount();
	// add word count to level and session statistics
	m_levelStats.m_words += m_wordsInCurrentLine;
	m_sessionStats.m_words += m_wordsInCurrentLine;
	// are there level stats to be stored?
	if (m_levelStats.m_elapsedTime != 0)  {
		//kdDebug() << "[KTouchTrainer::storeTrainingStatistics]  Storing level statistics!" << endl;
		m_levelStats.m_timeRecorded = TQDateTime::currentDateTime();
		data.m_levelStats.push_back( m_levelStats );
	}
	if (m_sessionStats.m_elapsedTime != 0) {
	   //kdDebug() << "[KTouchTrainer::storeTrainingStatistics]  Storing session statistics!" << endl;
		m_sessionStats.m_timeRecorded = TQDateTime::currentDateTime();
		data.m_sessionStats.push_back( m_sessionStats );
	}
}
// ----------------------------------------------------------------------------

bool KTouchTrainer::studentLineCorrect() const { 
    if (m_teacherText.isEmpty())
        return m_studentText.isEmpty();
	unsigned int len = m_studentText.length();
    // different check for left and right writing
    if (Prefs::right2LeftTyping())
        return m_teacherText.right(len)==m_studentText && m_teacherText.length()>=len;
    else
	   return (m_teacherText.left(len)==m_studentText && m_teacherText.length()>=len); 
}
// ----------------------------------------------------------------------------


// *** Public slots ***

void KTouchTrainer::levelUp() {
    KAudioPlayer::play(m_levelUpSound.url());
    ++m_level;  // increase the level
    if (m_level>=m_lecture->levelCount()) {
        // already at max level? Let's stay there
        m_level=m_lecture->levelCount()-1;
	levelAllComplete();
		/// \todo Do something when last level is completed
    }
	// Store level statistics if level is increased
	statsChangeLevel();
	gotoFirstLine();
	updateLevelChangeButtons();
}


// ----------------------------------------------------------------------------

void KTouchTrainer::levelDown() {
	if (m_level>0) {
       --m_level;
	   KAudioPlayer::play(m_levelDownSound.url());
	}
	// Store level statistics if level is increased
	statsChangeLevel();
	gotoFirstLine();
	updateLevelChangeButtons();
}
// ----------------------------------------------------------------------------

void KTouchTrainer::timerTick() {
    if (m_trainingPaused) return;
    // Add the timer interval. I think we can neglect the error we make when the session is
    // paused and continued... it's not a scientific calculation, isn't it?
	statsAddTime(LCD_UPDATE_INTERVAL*0.001);
	// update only the widgets that are affected by progressing time
    m_statusWidget->speedLCD->display( m_levelStats.charSpeed() );
}
// ----------------------------------------------------------------------------



// *** Private functions ***

void KTouchTrainer::levelAllComplete() {
    TQMessageBox::information(0, i18n("You rock!"),
                   i18n("You have finished this training exercise.\n"
                      "This training session will start from the beginning."));
    statsChangeLevel();
    startTraining(false);
}

void KTouchTrainer::updateLevelChangeButtons() {
    if (!Prefs::disableManualLevelChange()) {
       m_statusWidget->levelUpBtn->setEnabled(m_level < m_lecture->levelCount() - 1);
       m_statusWidget->levelDownBtn->setEnabled(m_level > 0);
    }
}
void KTouchTrainer::newLine() {
    m_teacherText = m_lecture->level(m_level).line(m_line);
    m_studentText="";
	m_wordsInCurrentLine = 0; 
    m_keyboardWidget->newKey(m_teacherText[0]);
    m_slideLineWidget->setNewText(m_teacherText, m_studentText);
	updateStatusBar();	// update status bar
}
// ----------------------------------------------------------------------------

void KTouchTrainer::updateStatusBar() const {
	KTouchPtr->changeStatusbarStats(m_levelStats.m_correctChars, m_levelStats.m_totalChars, 
		m_levelStats.m_words + m_wordsInCurrentLine,
		m_sessionStats.m_correctChars, m_sessionStats.m_totalChars, 
		m_sessionStats.m_words + m_wordsInCurrentLine);
}
// ----------------------------------------------------------------------------

void KTouchTrainer::updateStatusBarMessage(const TQString& message) const {
	KTouchPtr->changeStatusbarMessage(message);
}
// ----------------------------------------------------------------------------

void KTouchTrainer::updateWordCount() {
	// now update the m_wordsInCurrentLine variable
	if (!studentLineCorrect()) return;	// if error, don't update
	int words = 0;
	bool space = true;
	for (unsigned int i=0; i<m_studentText.length(); ++i) {
		bool is_space = (m_studentText[i] == TQChar(' '));
		if (is_space) {
		 	if (space) continue; // two spaces after each other... ignore
			else {
				++words;
				space = true;
			}
		}
		else {
			if (!space) continue; // two chars after each other... ignore
			else {
				space = false;	// no need to add a word here.
			}
		}
	}
	// check if line is completely typed and add a word then
	if (m_studentText == m_teacherText) ++words;
	m_wordsInCurrentLine = words;
}
// ----------------------------------------------------------------------------

void KTouchTrainer::statsAddCorrectChar(TQChar key) {
	m_levelStats.addCorrectChar(key);
	m_sessionStats.addCorrectChar(key);
}
// ----------------------------------------------------------------------------

void KTouchTrainer::statsAddWrongChar(TQChar key) {
	m_levelStats.addWrongChar(key);
	m_sessionStats.addWrongChar(key);
}
// ----------------------------------------------------------------------------

void KTouchTrainer::statsRemoveCorrectChar(TQChar) {
	m_levelStats.removeCorrectChar();
	m_sessionStats.removeCorrectChar();
}
// ----------------------------------------------------------------------------

void KTouchTrainer::statsAddTime(double dt) {
	m_levelStats.m_elapsedTime += dt;
	m_sessionStats.m_elapsedTime += dt;
}
// ----------------------------------------------------------------------------

void KTouchTrainer::statsChangeLevel() {
	//kdDebug() << "[KTouchTrainer::statsChangeLevel]  First!" << endl;
	// first update word count and store data in 
	updateWordCount();
	//kdDebug() << "[KTouchTrainer::statsChangeLevel]  Adding word count of " << m_wordsInCurrentLine << endl;
	m_levelStats.m_words += m_wordsInCurrentLine;
	m_sessionStats.m_words += m_wordsInCurrentLine;
	// get a reference to the statistics data of the current lecture
	KTouchLectureStats& data = KTouchPtr->getCurrentLectureStats();
	// are there level stats to be stored?
	if (m_levelStats.m_elapsedTime != 0)  {
		//kdDebug() << "[KTouchTrainer::storeTrainingStatistics]  Storing level statistics!" << endl;
		m_levelStats.m_timeRecorded = TQDateTime::currentDateTime();
		data.m_levelStats.push_back( m_levelStats );
	}
	// clear level stats
	m_levelStats.clear();
	// transfer current level to level statistics
	m_levelStats.m_levelNum = m_level;
	// remember level in session stats
	m_sessionStats.m_levelNums.insert(m_level);
	// show new level (in status widet) and 100% correctness
	m_statusWidget->updateStatus(m_level, 1); 
}
// ----------------------------------------------------------------------------
