/**********************************************************************
** 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 "categories.h"
#include <qfile.h>
#include <qcstring.h>
#include <qtextstream.h>
#include "stringutil.h"

using namespace Qtopia;

/***********************************************************
 *
 * CategoryGroup
 *
 **********************************************************/

#ifdef PALMTOPCENTER
UidGen CategoryGroup::sUidGen( UidGen::PalmtopCenter );
#else
UidGen CategoryGroup::sUidGen( UidGen::Qtopia );
#endif

int CategoryGroup::add( const QString &label )
{
    if ( label == QObject::tr("All") || label == QObject::tr("Unfiled") )
	return 0;

    QMap<QString,int>::Iterator findIt = mLabelIdMap.find( label );
    if ( findIt != mLabelIdMap.end() )
	return 0;
    int newUid = uidGen().generate();
    insert( newUid, label );
    return newUid;
}

void CategoryGroup::insert( int uid, const QString &label )
{
    uidGen().store( uid );
    mIdLabelMap[uid] = label;
    mLabelIdMap[label] = uid;
}

bool CategoryGroup::add( int uid, const QString &label )
{
    if ( label == QObject::tr("All") || label == QObject::tr("Unfiled") )
	return FALSE;

    QMap<QString,int>::ConstIterator labelIt = mLabelIdMap.find( label );
    if ( labelIt != mLabelIdMap.end() )
	return FALSE;
    QMap<int,QString>::ConstIterator idIt = mIdLabelMap.find( uid );
    if ( idIt != mIdLabelMap.end() )
	return FALSE;
    insert( uid, label );
    return TRUE;
}

bool CategoryGroup::remove( const QString &label )
{
    QMap<QString,int>::Iterator findIt = mLabelIdMap.find( label );
    if ( findIt == mLabelIdMap.end() )
	return FALSE;

    mIdLabelMap.remove( *findIt );
    mLabelIdMap.remove( findIt );

    return TRUE;
}

bool CategoryGroup::remove( int uid )
{
    QMap<int,QString>::Iterator idIt = mIdLabelMap.find( uid );
    if ( idIt == mIdLabelMap.end() )
	return FALSE;

    mLabelIdMap.remove( *idIt );
    mIdLabelMap.remove( idIt );

    return TRUE;
}

bool CategoryGroup::rename( int uid, const QString &newLabel )
{
    if ( newLabel == QObject::tr("All") || newLabel == QObject::tr("Unfiled") )
	return FALSE;

    QMap<int, QString>::Iterator idIt = mIdLabelMap.find( uid );
    if ( idIt == mIdLabelMap.end() )
	return FALSE;

    mLabelIdMap.remove( *idIt );
    mLabelIdMap[newLabel] = uid;
    *idIt = newLabel;

    return TRUE;
}

bool CategoryGroup::rename( const QString &oldLabel, const QString &newLabel )
{
    return rename( id(oldLabel), newLabel );
}

bool CategoryGroup::contains(int uid) const
{
    return ( mIdLabelMap.find( uid ) != mIdLabelMap.end() );
}

bool CategoryGroup::contains(const QString &label) const
{
    return ( mLabelIdMap.find( label ) != mLabelIdMap.end() );
}

/** Returns label associated with the uid or QString::null if
 *   not found
 */
const QString &CategoryGroup::label(int uid) const
{
    QMap<int,QString>::ConstIterator idIt = mIdLabelMap.find( uid );
    if ( idIt == mIdLabelMap.end() )
	return QString::null;
    return *idIt;
}

/** Returns the uid associated with label or 0 if not found */
int CategoryGroup::id(const QString &label) const
{
    QMap<QString,int>::ConstIterator labelIt = mLabelIdMap.find( label );
    if ( labelIt == mLabelIdMap.end() )
	return 0;
    return *labelIt;
}

QStringList CategoryGroup::labels() const
{
    QStringList labels;
    for ( QMap<int, QString>::ConstIterator it = mIdLabelMap.begin();
	  it != mIdLabelMap.end(); ++it )
	labels += *it;
    // ### I don't think this is the place for this...
//    labels.sort();
    return labels;
}

QStringList CategoryGroup::labels(const QArray<int> &catids ) const
{
    QStringList labels;
    if ( catids.count() == 0 )
	return labels;
    for ( QMap<int, QString>::ConstIterator it = mIdLabelMap.begin();
	  it != mIdLabelMap.end(); ++it )
	if ( catids.find( it.key() ) != -1 )
	    labels += *it;
    return labels;
}

QArray<int> CategoryGroup::ids( const QStringList &cats ) const
{
    QArray<int> results;

    for ( QStringList::ConstIterator catIt = cats.begin();
	  catIt != cats.end(); ++catIt ) {
	if ( *catIt == QObject::tr("All") || *catIt == QObject::tr("Unfiled") )
	    continue;
	int value = id( *catIt );
	if ( value != 0 ) {
	    int tmp = results.size();
	    results.resize( tmp + 1 );
	    results[ tmp ] = value;
	}
    }

    return results;
}

QArray<int> CategoryGroup::ids() const
{
    QArray<int> results( mIdLabelMap.count() );
    int i = 0;
    for ( QMap<int, QString>::ConstIterator it = mIdLabelMap.begin();
	  it != mIdLabelMap.end(); ++it )
	results[i++] = it.key();

    return results;
}

/***********************************************************
 *
 * Categories
 *
 **********************************************************/

/** Add the category name as long as it doesn't already exist locally
 *  or globally.  Return TRUE if added, FALSE if conflicts.
 */
int Categories::addCategory( const QString &appname,
			     const QString &catname,
			     int uid )
{
    if ( mGlobalCats.contains(catname) )
	return 0;

    QMap< QString, CategoryGroup >::Iterator
	appIt = mAppCats.find( appname );

    if ( appIt == mAppCats.end() ) {
	CategoryGroup newgroup;
	newgroup.add( uid, catname );
	mAppCats.insert( appname, newgroup );
	emit categoryAdded( *this, appname, uid );
	return uid;
    }

    CategoryGroup &cats = *appIt;
    cats.add( uid, catname );
    emit categoryAdded( *this, appname, uid );
    return uid;
}

int Categories::addCategory( const QString &appname,
			     const QString &catname )
{
     if ( mGlobalCats.contains(catname) )
	return 0;

    QMap< QString, CategoryGroup >::Iterator
	appIt = mAppCats.find( appname );

    if ( appIt == mAppCats.end() ) {
	CategoryGroup newgroup;
	int uid = newgroup.add( catname );
	mAppCats.insert( appname, newgroup );
	emit categoryAdded( *this, appname, uid );
	return uid;
    }

    CategoryGroup &cats = *appIt;
    int uid = cats.add( catname );
    if ( !uid )
	return 0;
    emit categoryAdded( *this, appname, uid );
    return uid;
}

int Categories::addGlobalCategory( const QString &catname, int uid )
{
    mGlobalCats.add( uid, catname );
    emit categoryAdded( *this, QString::null, uid );
    return uid;
}

int Categories::addGlobalCategory( const QString &catname )
{
    int uid = mGlobalCats.add( catname );
    if ( !uid )
	return 0;
    emit categoryAdded( *this, QString::null, uid );
    return uid;
}

/** Removes the category from the application; if it is not found
 *  in the application, then it attempts to remove it from
 *  the global list
 */
bool Categories::removeCategory( const QString &appname,
				 const QString &catname,
				 bool checkGlobal )
{
    QMap< QString, CategoryGroup >::Iterator
	appIt = mAppCats.find( appname );
    if ( appIt != mAppCats.end() ) {
	CategoryGroup &cats = *appIt;
	int uid = cats.id( catname );
	if ( cats.remove( uid ) ) {
	    emit categoryRemoved( *this, appname, uid );
	    return TRUE;
	}
    }
    if ( !checkGlobal )
	return FALSE;
    return removeGlobalCategory( catname );
}

bool Categories::removeCategory( const QString &appname, int uid )
{
    QMap< QString, CategoryGroup >::Iterator
	appIt = mAppCats.find( appname );
    if ( appIt != mAppCats.end() ) {
	CategoryGroup &cats = *appIt;
	if ( cats.remove( uid ) ) {
	    emit categoryRemoved( *this, appname, uid );
	    return TRUE;
	}
    }
    return FALSE;
}

bool Categories::removeGlobalCategory( const QString &catname )
{
    int uid = mGlobalCats.id( catname );
    if ( mGlobalCats.remove( uid ) ) {
	emit categoryRemoved( *this, QString::null, uid );
	return TRUE;
    }
    return FALSE;
}


bool Categories::removeGlobalCategory( int uid )
{
     if ( mGlobalCats.remove( uid ) ) {
	emit categoryRemoved( *this, QString::null, uid );
	return TRUE;
    }
    return FALSE;
}

/** Returns the sorted list of all categories that are associated with
 *   the app.  If includeGlobal parameter is TRUE then the returned
 *   categories will include the global category items.
 */
QStringList Categories::labels( const QString &app,
				bool includeGlobal,
				ExtraLabels extra ) const
{
    QMap< QString, CategoryGroup >::ConstIterator
	appIt = mAppCats.find( app );
    QStringList cats;
    switch ( extra ) {
    case NoExtra: break;
    case AllUnfiled:
	cats.append( tr("All") );
	cats.append( tr("Unfiled") );
	break;
    case AllLabel:
	cats.append( tr("All") );
	break;
    case UnfiledLabel:
	cats.append( tr("Unfiled") );
	break;
    }
    if ( appIt != mAppCats.end() )
	cats += (*appIt).labels();
    else qDebug("Categories::labels didn't find app %s", app.latin1() );
    if ( includeGlobal )
	cats += mGlobalCats.labels();
    // I don't think a sorted list is useful, the user might find prefer
    // it in the original order.
//     cats.sort();
    return cats;
}

QString Categories::label( const QString &app, int id ) const
{
    if ( mGlobalCats.contains( id ) )
	return mGlobalCats.label( id );
    QMap< QString, CategoryGroup >::ConstIterator
	appIt = mAppCats.find( app );
    if ( appIt == mAppCats.end() )
	return QString::null;
    return (*appIt).label( id );
}

QStringList Categories::labels( const QString & app,
				const QArray<int> &catids ) const
{
    QStringList strs = mGlobalCats.labels( catids );
    strs += mAppCats[app].labels( catids );
    return strs;
}

/** Returns a single string associated with the cat ids for display in
    *   a combobox or any area that requires one string.  If catids are empty
    *   then "Unfiled" will be returned.  If multiple categories are assigned
    *   the first cat id is shown with " (multi)" appended to the string.
    */
QString Categories::displaySingle( const QString &app,
				   const QArray<int> &catids,
				   DisplaySingle display ) const
{
    QStringList strs = labels( app, catids );
    if ( !strs.count() )
	return tr("Unfiled");
    strs.sort();
    QString r;
    if ( strs.count() > 1 ) {
	switch ( display ) {
	case ShowFirst:
	    r = strs.first();
	    break;
	case ShowMulti:
	    r = strs.first() + tr(" (multi.)");
	    break;
	case ShowAll:
	    r = strs.join(" ");
	    break;
	}
    }
    else r = strs.first(); 	
    return r;
}

QArray<int> Categories::ids( const QString &app ) const
{
    QArray<int> allIds = mGlobalCats.ids();
    QArray<int> appIds = mAppCats[app].ids();

    // we should make the guarentee that the ids are in the
    // same order as the labels, (i.e. app cats then global)
    // otherwise there is no point in having these two separate functions.
    uint appSize = appIds.size();
    appIds.resize( appSize + allIds.size() );
    for ( uint i = appSize; i < appIds.size(); ++i )
	appIds[int(i)] = allIds[int(i - appSize)];

    return appIds;
}

QArray<int> Categories::ids( const QString &app, const QStringList &cats ) const
{
    QArray<int> allIds = mGlobalCats.ids( cats );
    QArray<int> appIds = mAppCats[app].ids( cats );

    uint appSize = appIds.size();
    appIds.resize( appSize + allIds.size() );
    for ( uint i = appSize; i < appIds.size(); ++i )
	appIds[int(i)] = allIds[int(i - appSize)];

    return appIds;
}

int Categories::id( const QString &app, const QString &cat ) const
{
    if ( cat == tr("Unfiled") || cat.contains( tr(" (multi.)") ) )
	return 0;
    int uid = mGlobalCats.id( cat );
    if ( uid != 0 )
	return uid;
    return mAppCats[app].id( cat );
}


/** Return TRUE if renaming succeeded; FALSE if app name not found,
 *   or if there was a name conflict
 */
bool Categories::renameCategory( const QString &appname,
				 const QString &oldName,
				 const QString &newName )
{
    QMap< QString, CategoryGroup >::Iterator
	appIt = mAppCats.find( appname );

    if ( appIt != mAppCats.end() ) {
	CategoryGroup &cats = *appIt;
	int id = cats.id( oldName );
	if ( id != 0 && cats.rename( id, newName ) ) {
	    emit categoryRenamed( *this, appname, id );
	    return TRUE;
	}
    }
    return renameGlobalCategory( oldName, newName );
}

bool Categories::renameGlobalCategory( const QString &oldName,
				       const QString &newName )
{
    int uid = mGlobalCats.id( oldName );
    if ( uid != 0 && mGlobalCats.rename( uid, newName ) ) {
	emit categoryRenamed( *this, QString::null, uid );
	return TRUE;
    }
    return FALSE;
}

void Categories::setGlobal( const QString &appname,
			    const QString &catname,
			    bool global )
{
    // if in global and should be in app; then move it
    if ( mGlobalCats.contains( catname ) && !global ) {
	mGlobalCats.remove( catname );
	addCategory( appname, catname );
	return ;
    }

    // if in app and should be in global, then move it
    if ( !global )
	return;
    if ( removeCategory( appname, catname, FALSE ) )
	addGlobalCategory( catname );
}

bool Categories::isGlobal( const QString &catname ) const
{
    return mGlobalCats.contains( catname );
}


/** Returns true if the catname is associated with any application
 */
bool Categories::exists( const QString &catname ) const
{
    if ( isGlobal(catname) )
	return TRUE;

    for ( QMap<QString, CategoryGroup>::ConstIterator appsIt = mAppCats.begin(); appsIt != mAppCats.end(); ++appsIt )
	if ( exists( appsIt.key(), catname ) )
	    return TRUE;

    return FALSE;
}

bool Categories::exists( const QString &appname,
			 const QString &catname) const
{
    QMap< QString, CategoryGroup >::ConstIterator
	appIt = mAppCats.find( appname );

    if ( appIt == mAppCats.end() )
	return FALSE;

    return (*appIt).contains( catname );
}

bool Categories::save( const QString &fname ) const
{
    QFile file( fname );
    if ( !file.open( IO_WriteOnly ) ) {
	qWarning("Unable to write to %s", fname.latin1());
	return FALSE;
    }

    QTextStream ts( &file );
    ts << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    ts << "<!DOCTYPE CategoryList>" << endl;

    ts << "<Categories>" << endl;
    for ( QMap<int, QString>::ConstIterator git = mGlobalCats.idMap().begin();
	  git != mGlobalCats.idMap().end(); ++git )
	ts << "<Category id=\"" << git.key() << "\""
	   << " name=\"" << escapeString(*git) << "\" />" << endl;

    for ( QMap<QString, CategoryGroup>::ConstIterator appsIt=mAppCats.begin();
	  appsIt != mAppCats.end(); ++appsIt ) {
	const QString &app = appsIt.key();
	const QMap<int, QString> &appcats = (*appsIt).idMap();
	for ( QMap<int, QString>::ConstIterator appcatit = appcats.begin();
	      appcatit != appcats.end(); ++appcatit )
	    ts << "<Category id=\"" << appcatit.key() << "\""
	       << " app=\"" << escapeString(app) << "\""
	       << " name=\"" << escapeString(*appcatit) << "\" />" << endl;
    }
    ts << "</Categories>" << endl;

    file.close();
    return TRUE;
}

bool Categories::load( const QString &fname )
{
    QFile file( fname );
    if ( !file.open( IO_ReadOnly ) ) {
	qWarning("Unable to open %s", fname.latin1());
	return FALSE;
    }

    clear();
    QByteArray ba = file.readAll();
    QString data = QString::fromUtf8( ba.data(), ba.size() );
    QChar *uc = (QChar *)data.unicode();
    int len = data.length();

    //     QTime t;
    //     t.start();
    QString name;
    QString id;
    QString app;
    int i = 0;
    while ( (i = data.find( "<Category ",  i)) != -1 ) {

	i += 10;
	name = QString::null;
	app = QString::null;
	while ( 1 ) {
	    // skip white space
	    while ( i < len &&
		    (uc[i] == ' ' || uc[i] == '\n' || uc[i] == '\r') )
		i++;
	    // if at the end, then done
	    if ( i >= len-2 || (uc[i] == '/' && uc[i+1] == '>') )
		break;
	    // we have another attribute read it.
	    int j = i;
	    while ( j < len && uc[j] != '=' )
		j++;
	    QString attr = QConstString( uc+i, j-i ).string();
	    i = ++j; // skip =
	    while ( i < len && uc[i] != '"' )
		i++;
	    j = ++i;
	    while ( j < len && uc[j] != '"' )
		j++;
	    QString value = Qtopia::plainString( QConstString( uc+i, j-i ).string() );
	    i = j + 1;

//  	    qDebug("attr='%s' value='%s'", attr.latin1(), value.latin1() );
	    if ( attr == "id" )
		id = value;
	    else if ( attr == "app" )
		app = value;

	    else if ( attr == "name" )
		name = value;
	}

	if ( name.isNull() || id.isNull() ) {
	    qWarning("No name or id in the category");
	    continue;
	}
	if ( app.isNull() )
	    mGlobalCats.add( id.toInt(), name );
	else
	    mAppCats[ app ].add( id.toInt(), name );
    }

    return TRUE;
}

void Categories::clear()
{
    mGlobalCats.clear();
    mAppCats.clear();
}

void Categories::dump() const
{
    qDebug("\tglobal categories = %s", mGlobalCats.labels().join(", ").latin1() );
    for ( QMap<QString, CategoryGroup>::ConstIterator appsIt = mAppCats.begin(); appsIt != mAppCats.end(); ++appsIt ) {
	const QString &app = appsIt.key();
	QStringList appcats = (*appsIt).labels();
	qDebug("\tapp = %s\tcategories = %s", app.latin1(),
	       appcats.join(", ").latin1() );

    }
}

QStringList CheckedListView::checked() const
{
    QStringList strs;
    for ( QCheckListItem *i = (QCheckListItem *) firstChild();
	  i; i = (QCheckListItem *)i->nextSibling() )
	if ( i->isOn() )
	    strs += i->text( 0 );
    return strs;
}

void CheckedListView::addCheckableList( const QStringList &options )
{
    for ( QStringList::ConstIterator it = options.begin();
	  it != options.end(); ++it ) {
	(void) new QCheckListItem( this, *it,
				   QCheckListItem::CheckBox );
    }
}

void CheckedListView::setChecked( const QStringList &checked )
{
    // iterate over all items
    bool showingChecked = FALSE;
    for ( QCheckListItem *i = (QCheckListItem *) firstChild();
	  i; i = (QCheckListItem *)i->nextSibling() )
	// see if the item should be checked by searching the
	// checked list
	if ( checked.find( i->text( 0 ) ) != checked.end() ) {
	   i->setOn( TRUE );
	   // make sure it is showing at least one checked item
	   if ( !showingChecked ) {
	       ensureItemVisible( i );
	       showingChecked = TRUE;
	   }
	}
       else
	   i->setOn( FALSE );
}