/**********************************************************************
 ** Copyright (C) 2000 Trolltech AS.  All rights reserved.
 **
 ** This file is part of Qtopia Environment.
 **
 ** This file may be distributed and/or modified under the terms of the
 ** GNU General Public License version 2 as published by the Free Software
 ** Foundation and appearing in the file LICENSE.GPL included in the
 ** packaging of this file.
 **
 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 **
 ** See http://www.trolltech.com/gpl/ for GPL licensing information.
 **
 ** Contact info@trolltech.com if any conditions of this licensing are
 ** not clear to you.
 **
 **********************************************************************/

#include <qfile.h>
#include <qtl.h>
#include <math.h>
#include <limits.h>
#include <errno.h>
#include <qdatastream.h>
#include "qimpencombining.h"
#include "qimpenchar.h"
#include "opie2/odebug.h"

#define QIMPEN_MATCH_THRESHOLD	    200000

const QIMPenSpecialKeys qimpen_specialKeys[] = {
	{ Qt::Key_Escape,		"[Esc]" },
	{ Qt::Key_Tab,		"[Tab]" },
	{ Qt::Key_Backspace,	"[BackSpace]" },
	{ Qt::Key_Return,		"[Return]" },
	{ QIMPenChar::Caps,		"[Uppercase]" },
	{ QIMPenChar::CapsLock,	"[Caps Lock]" },
	{ QIMPenChar::Shortcut,	"[Shortcut]" },
	{ QIMPenChar::Punctuation,  "[Punctuation]" },
	{ QIMPenChar::Symbol,	"[Symbol]" },
	{ QIMPenChar::Extended,	"[Extended]" },
	{ Qt::Key_unknown,	        0 } };


/*!
  \class QIMPenChar qimpenchar.h

  Handles a single character.  Can calculate closeness of match to
  another character.
  */

QIMPenChar::QIMPenChar()
{
	flags = 0;
	strokes.setAutoDelete( TRUE );
}

QIMPenChar::QIMPenChar( const QIMPenChar &chr )
{
	strokes.setAutoDelete( TRUE );
	ch = chr.ch;
	flags = chr.flags;
	d = chr.d;
	QIMPenStrokeIterator it( chr.strokes );
	while ( it.current() ) {
		strokes.append( new QIMPenStroke( *it.current() ) );
		++it;
	}
}

QIMPenChar &QIMPenChar::operator=( const QIMPenChar &chr )
{
	strokes.clear();
	ch = chr.ch;
	flags = chr.flags;
	d = chr.d;
	QIMPenStrokeIterator it( chr.strokes );
	while ( it.current() ) {
		strokes.append( new QIMPenStroke( *it.current() ) );
		++it;
	}

	return *this;
}

QString QIMPenChar::name() const
{
	QString n;

	if ( (ch & 0x0000FFFF) == 0 ) {
		int code = ch >> 16;
		for ( int i = 0; qimpen_specialKeys[i].code != Qt::Key_unknown; i++ ) {
			if ( qimpen_specialKeys[i].code == code ) {
				n = qimpen_specialKeys[i].name;
				break;
			}
		}
	} else {
		n = QChar( ch & 0x0000FFFF );
	}

	return n;
}

void QIMPenChar::clear()
{
	ch = 0;
	flags = 0;
	d = QString::null;
	strokes.clear();
}

unsigned int QIMPenChar::strokeLength( int s ) const
{
	QIMPenStrokeIterator it( strokes );
	while ( it.current() && s ) {
		++it;
		--s;
	}

	if ( it.current() )
		return it.current()->length();

	return 0;
}

/*!
  Add a stroke to the character
  */
void QIMPenChar::addStroke( QIMPenStroke *st )
{
	QIMPenStroke *stroke = new QIMPenStroke( *st );
	strokes.append( stroke );
}

/*!
  Return an indicator of the closeness of this character to \a pen.
  Lower value is better.
  */
int QIMPenChar::match( QIMPenChar *pen )
{
	/*
	   if ( strokes.count() > pen->strokes.count() )
	   return INT_MAX;
	   */
	int err = 0;
	int maxErr = 0;
	int diff = 0;
	QIMPenStrokeIterator it1( strokes );
	QIMPenStrokeIterator it2( pen->strokes );
	err = it1.current()->match( it2.current() );
	if ( err > maxErr )
		maxErr = err;
	++it1;
	++it2;
	while ( err < 400000 && it1.current() && it2.current() ) {
		QPoint p1 = it1.current()->boundingRect().center() -
			strokes.getFirst()->boundingRect().center();
		QPoint p2 = it2.current()->boundingRect().center() -
			pen->strokes.getFirst()->boundingRect().center();
		int xdiff = QABS( p1.x() - p2.x() ) - 6;
		int ydiff = QABS( p1.y() - p2.y() ) - 5;
		if ( xdiff < 0 )
			xdiff = 0;
		if ( ydiff < 0 )
			ydiff = 0;
		if ( xdiff > 10 || ydiff > 10 ) { // not a chance
#ifdef DEBUG_QIMPEN
			odebug << "char " << pen->ch <<", stroke starting pt diff excessive" << oendl;
#endif
			return INT_MAX;
		}
		diff += xdiff*xdiff + ydiff*ydiff;
		err = it1.current()->match( it2.current() );
		if ( err > maxErr )
			maxErr = err;
		++it1;
		++it2;
	}

	maxErr += diff * diff * 6; // magic weighting :)

#ifdef DEBUG_QIMPEN
	odebug << "char: " << pen->ch << ", maxErr " << maxErr << ", diff " << diff << ", "  << strokes.count() << oendl;
#endif
	return maxErr;
}

/*!
  Return the bounding rect of this character.  It may have sides with
  negative coords since its origin is where the user started drawing
  the character.
  */
QRect QIMPenChar::boundingRect()
{
	QRect br;
	QIMPenStroke *st = strokes.first();
	while ( st ) {
		br |= st->boundingRect();
		st = strokes.next();
	}

	return br;
}


/*!
  Write the character's data to the stream.
  */
QDataStream &operator<< (QDataStream &s, const QIMPenChar &ws)
{
	s << ws.ch;
	s << ws.flags;
	if ( ws.flags & QIMPenChar::Data )
		s << ws.d;
	s << ws.strokes.count();
	QIMPenStrokeIterator it( ws.strokes );
	while ( it.current() ) {
		s << *it.current();
		++it;
	}

	return s;
}

/*!
  Read the character's data from the stream.
  */
QDataStream &operator>> (QDataStream &s, QIMPenChar &ws)
{
	s >> ws.ch;
	s >> ws.flags;
	if ( ws.flags & QIMPenChar::Data )
		s >> ws.d;
	unsigned size;
	s >> size;
	for ( unsigned i = 0; i < size; i++ ) {
		QIMPenStroke *st = new QIMPenStroke();
		s >> *st;
		ws.strokes.append( st );
	}

	return s;
}

//===========================================================================

bool QIMPenCharMatch::operator>( const QIMPenCharMatch &m )
{
	return error > m.error;
}

bool QIMPenCharMatch::operator<( const QIMPenCharMatch &m )
{
	return error < m.error;
}

bool QIMPenCharMatch::operator<=( const QIMPenCharMatch &m )
{
	return error <= m.error;
}

//===========================================================================

/*!
  \class QIMPenCharSet qimpenchar.h

  Maintains a set of related characters.
  */

QIMPenCharSet::QIMPenCharSet()
{
	chars.setAutoDelete( TRUE );
	desc = "Unnamed";
	csTitle = "abc";
	csType = Unknown;
	maxStrokes = 0;
}

/*!
  Construct and load a characters set from file \a fn.
  */
QIMPenCharSet::QIMPenCharSet( const QString &fn )
{
	chars.setAutoDelete( TRUE );
	desc = "Unnamed";
	csTitle = "abc";
	csType = Unknown;
	maxStrokes = 0;
	load( fn, System );
}

const QString &QIMPenCharSet::filename( Domain d ) const
{
	if ( d == System )
		return sysFilename;
	else
		return userFilename;
}

void QIMPenCharSet::setFilename( const QString &fn, Domain d )
{
	if ( d == System )
		sysFilename = fn;
	else if ( d == User )
		userFilename = fn;
}

/*!
  Load a character set from file \a fn.
  */
bool QIMPenCharSet::load( const QString &fn, Domain d )
{
	setFilename( fn, d );

	bool ok = FALSE;
	QFile file( fn );
	if ( file.open( IO_ReadOnly ) ) {
		QDataStream ds( &file );
		QString version;
		ds >> version;
		ds >> csTitle;
		ds >> desc;
		int major = version.mid( 4, 1 ).toInt();
		int minor = version.mid( 6 ).toInt();
		if ( major >= 1 && minor > 0 ) {
			ds >> (Q_INT8 &)csType;
		} else {
			if ( csTitle == "abc" )
				csType = Lower;
			else if ( csTitle == "ABC" )
				csType = Upper;
			else if ( csTitle == "123" )
				csType = Numeric;
			else if ( fn == "Combining" )
				csType = Combining;
		}
		while ( !ds.atEnd() ) {
			QIMPenChar *pc = new QIMPenChar;
			ds >> *pc;
			if ( d == User )
				markDeleted( pc->character() ); // override system
			if ( !pc->testFlag( QIMPenChar::Deleted ) )
				addChar( pc );
		}
		if ( file.status() == IO_Ok )
			ok = TRUE;
	}
	setHidden ( false ); 
	return ok;
}

/*!
  Save this character set.
  */
bool QIMPenCharSet::save( Domain d )
{
	if ( filename( d ).isEmpty() )
		return FALSE;

	if ( hidden() )
		return TRUE;

	bool ok = FALSE;

	QString fn = filename( d );
	QString tmpFn = fn + ".new";
	QFile file( tmpFn );
	if ( file.open( IO_WriteOnly|IO_Raw ) ) {
        QByteArray buf;
        QDataStream ds( buf, IO_WriteOnly );
		ds << QString( "QPT 1.1" );
		ds << csTitle;
		ds << desc;
		ds << (Q_INT8)csType;
		QIMPenCharIterator ci( chars );
		for ( ; ci.current(); ++ci ) {
			QIMPenChar *pc = ci.current();
/*
 * If the Domain is System and the Char is marked System - OR
 * the domain is User, the Char is User and it's not deleted - OR
 * the domain is User, the Char is System and it is deleted - AND
 * the character is not an automated Combined Character
 * 
 * This is required to ensure that we don't save user defined chars that have been deleted, but
 * we *DO* save System chars that have been deleted. There is still the issue of deleted combined
 * chars but I'm not sure how to tackle that yet
 *
 */
 
		if ( ( ( (d == System) && pc->testFlag( QIMPenChar::System ) ) ||
		 ( (d == User) && !pc->testFlag( QIMPenChar::System ) && !pc->testFlag( QIMPenChar::Deleted ) ) ||
		 ( (d == User) && pc->testFlag( QIMPenChar::System ) && pc->testFlag( QIMPenChar::Deleted ) ) ) && 
                 ( !pc->testFlag (QIMPenChar::Combined ) ) ) {
				ds << *pc;
			}
		}

		file.writeBlock( buf );
		file.close();
		if ( file.status() == IO_Ok )
			ok = TRUE;
	}

	if ( ok ) {
		if ( ::rename( tmpFn.latin1(), fn.latin1() ) < 0 ) {
			owarn << "problem renaming file " <<tmpFn.latin1() << " to "<< fn.latin1()
			<< ", errno: " <<  errno << oendl;
			// remove the tmp file, otherwise, it will just lay around...
			QFile::remove( tmpFn.latin1() );
			ok = FALSE;
		}
	}

	return ok;
}

QIMPenChar *QIMPenCharSet::at( int i )
{
	return chars.at(i);
}

void QIMPenCharSet::markDeleted( uint ch )
{
	QIMPenCharIterator ci( chars );
	for ( ; ci.current(); ++ci ) {
		QIMPenChar *pc = ci.current();
		if ( pc->character() == ch && pc->testFlag( QIMPenChar::System ) )
			pc->setFlag( QIMPenChar::Deleted );
	}
}

/*!
  Find the best matches for \a ch in this character set.
  */
QIMPenCharMatchList QIMPenCharSet::match( QIMPenChar *ch )
{
	QIMPenCharMatchList matches;

	QIMPenCharIterator ci( chars );
	for ( ; ci.current(); ++ci ) {
		QIMPenChar *tmplChar = ci.current();
		if ( tmplChar->testFlag( QIMPenChar::Deleted ) ) {
			continue;
		}
		int err;
		if ( ch->penStrokes().count() <= tmplChar->penStrokes().count() ) {
			err = ch->match( tmplChar );
			if ( err <= QIMPEN_MATCH_THRESHOLD ) {
				if (tmplChar->penStrokes().count() != ch->penStrokes().count())
					err = QMIN(err*3, QIMPEN_MATCH_THRESHOLD);
				QIMPenCharMatchList::Iterator it;
				for ( it = matches.begin(); it != matches.end(); ++it ) {
					if ( (*it).penChar->character() == tmplChar->character() &&
							(*it).penChar->penStrokes().count() == tmplChar->penStrokes().count() ) {
						if ( (*it).error > err )
							(*it).error = err;
						break;
					}
				}
				if ( it == matches.end() ) {
					QIMPenCharMatch m;
					m.error = err;
					m.penChar = tmplChar;
					matches.append( m );
				}
			}
		}
	}
	qHeapSort( matches );
	/*
	   QIMPenCharMatchList::Iterator it;
	   for ( it = matches.begin(); it != matches.end(); ++it ) {
	
	   odebug << "Match: \'" << (*it).penChar->character() "\', error " << (*it).error ", strokes " <<(*it).penChar->penStrokes().count() << oendl;
	   }
	   */
	return matches;
}

/*!
  Add a character \a ch to this set.
  QIMPenCharSet will delete this character when it is no longer needed.
  */
void QIMPenCharSet::addChar( QIMPenChar *ch )
{
	if ( ch->penStrokes().count() > maxStrokes )
		maxStrokes = ch->penStrokes().count();
	chars.append( ch );
}

/*!
  Remove a character by reference \a ch from this set.
  QIMPenCharSet will delete this character.
  */
void QIMPenCharSet::removeChar( QIMPenChar *ch )
{
	chars.remove( ch );
}

/*!
  Move the character up the list of characters.
  */
void QIMPenCharSet::up( QIMPenChar *ch )
{
	int idx = chars.findRef( ch );
	if ( idx > 0 ) {
		chars.take();
		chars.insert( idx - 1, ch );
	}
}

/*!
  Move the character down the list of characters.
  */
void QIMPenCharSet::down( QIMPenChar *ch )
{
	int idx = chars.findRef( ch );
	if ( idx >= 0 && idx < (int)chars.count() - 1 ) {
		chars.take();
		chars.insert( idx + 1, ch );
	}
}