/* This file is part of the KDE project
   Copyright (C) 2004 by Alexander Dymo <cloudtemple@mksat.net>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/
#include "kdevdocumentationplugin.h"

#include <qfile.h>
#include <qpainter.h>
#include <qstyle.h>
#include <qheader.h>
#include <qtextstream.h>

#include <kstandarddirs.h>
#include <kiconloader.h>
#include <kconfig.h>
#include <kdebug.h>
#include <kdirwatch.h>
#include <klocale.h>

//class DocumentationItem

DocumentationItem::DocumentationItem(DocumentationItem::Type type, KListView *parent,
    const QString &name)
    :KListViewItem(parent, name), m_type(type)
{
    init();
}

DocumentationItem::DocumentationItem(DocumentationItem::Type type, KListViewItem *parent,
    const QString &name)
    :KListViewItem(parent, name), m_type(type)
{
    init();
}

DocumentationItem::DocumentationItem(DocumentationItem::Type type, KListView *parent, 
    KListViewItem *after, const QString &name)
    :KListViewItem(parent, after, name), m_type(type)
{
    init();
}

DocumentationItem::DocumentationItem(DocumentationItem::Type type, KListViewItem * parent,
    KListViewItem * after, const QString & name )
    :KListViewItem(parent, after, name), m_type(type)
{
    init();
}


void DocumentationItem::init( )
{
    QString icon;
    
    switch (m_type)
    {
        case Collection:
        case Catalog:
            icon = "folder";
            break;
        case Book:
            icon = "contents";
            break;
        default:
            icon = "document";
    }
    
    setPixmap(0, SmallIcon(icon));
}





//class DocumentationCatalogItem

  
DocumentationCatalogItem::DocumentationCatalogItem(DocumentationPlugin* plugin,
    KListView *parent, const QString &name)
    :DocumentationItem(DocumentationItem::Catalog, parent, name), m_plugin(plugin),
    isLoaded(false), isActivated(false), m_isProjectDocumentationItem(false)
{
    setExpandable(true);
    m_plugin->addCatalog(this);
}

DocumentationCatalogItem::DocumentationCatalogItem(DocumentationPlugin* plugin,
    DocumentationItem *parent, const QString &name)
    :DocumentationItem(DocumentationItem::Catalog, parent, name), m_plugin(plugin),
    isLoaded(false), isActivated(false), m_isProjectDocumentationItem(false)
{
    setExpandable(true);
    m_plugin->addCatalog(this);
}

DocumentationCatalogItem::~ DocumentationCatalogItem( )
{
    m_plugin->clearCatalog(this);
}

void DocumentationCatalogItem::setOpen(bool o)
{
    if (o)
    {
        load();
    }
    DocumentationItem::setOpen(o);
}

void DocumentationCatalogItem::load()
{
if(isLoaded)
return;

        plugin()->createTOC(this);
        isLoaded = true;
}

void DocumentationCatalogItem::activate()
{
    if (!isActivated)
    {
        plugin()->setCatalogURL(this);
        isActivated = true;
    }
    DocumentationItem::activate();
}




//class IndexItemProto

IndexItemProto::IndexItemProto(DocumentationPlugin *plugin, DocumentationCatalogItem *catalog, 
    IndexBox *listbox, const QString &text, const QString &description)
    : m_listbox(listbox), m_text(text), m_description(description)
{
    plugin->indexes[catalog].append(this);
    m_listbox->addIndexItem(this);
}

IndexItemProto::~IndexItemProto()
{
    m_listbox->removeIndexItem(this);
}


//class IndexItem

IndexItem::IndexItem(IndexBox *listbox, const QString &text)
    :QListBoxText(listbox, text), m_listbox(listbox)
{
}

IndexItem::List IndexItem::urls() const
{
    List urlList;
    QValueList<IndexItemProto*> itemProtos = m_listbox->items[text()];
    for (QValueList<IndexItemProto*>::const_iterator it = itemProtos.begin();
        it != itemProtos.end(); ++it)
        urlList.append(qMakePair((*it)->description(), (*it)->url()));
    return urlList;
}




//class ConfigurationItem

ConfigurationItem::ConfigurationItem(QListView *parent, DocumentationPlugin * plugin, const QString &title, const QString &url,
    bool indexPossible, bool fullTextSearchPossible)
    :QCheckListItem(parent, "", QCheckListItem::CheckBox), m_title(title), m_url(url),
    m_origTitle(title), m_contents(true), m_index(false), m_fullTextSearch(false),
    m_indexPossible(indexPossible), m_fullTextSearchPossible(fullTextSearchPossible),
    m_docPlugin( plugin )
{
    setText(3, m_title);
    setText(4, m_url);
}

void ConfigurationItem::paintCell(QPainter *p, const QColorGroup &cg, int column,
    int width, int align)
{
    if ( (column == 0) || (column == 1) || (column == 2) )
    {
        if ( !p )
            return;
    
        QListView *lv = listView();
        if ( !lv )
            return;
    
        const BackgroundMode bgmode = lv->viewport()->backgroundMode();
        const QColorGroup::ColorRole crole = QPalette::backgroundRoleFromMode( bgmode );
        p->fillRect(0, 0, width, height(), cg.brush(crole));

        QFontMetrics fm(lv->fontMetrics());
        int boxsize = lv->style().pixelMetric(QStyle::PM_CheckListButtonSize, lv);
        int marg = lv->itemMargin();
        int styleflags = QStyle::Style_Default;
                
        if (((column == 0) && m_contents) || ((column == 1) && m_index) || ((column == 2) && m_fullTextSearch))
            styleflags |= QStyle::Style_On;
        else
            styleflags |= QStyle::Style_Off;
        if ((column == 0) || ((column == 1) && m_indexPossible) || ((column == 2) && m_fullTextSearchPossible))
            styleflags |= QStyle::Style_Enabled;

        int x = 0;
        int y = 0;
        x += 3;
        if (align & AlignVCenter)
            y = ((height() - boxsize) / 2) + marg;
        else
            y = (fm.height() + 2 + marg - boxsize) / 2;

        QStyleOption opt(this);
        lv->style().drawPrimitive(QStyle::PE_CheckListIndicator, p,
            QRect(x, y, boxsize, fm.height() + 2 + marg), cg, styleflags, opt);
               
        return;
    }
    QListViewItem::paintCell(p, cg, column, width, align);
}

int ConfigurationItem::width(const QFontMetrics &fm, const QListView *lv, int c) const
{
    if ((c == 0) || (c == 1) || (c == 2))
        return lv->style().pixelMetric(QStyle::PM_CheckListButtonSize, lv) + 4;
    return QListViewItem::width(fm, lv, c);
}








//class DocumentationPlugin

DocumentationPlugin::DocumentationPlugin(KConfig *pluginConfig, QObject *parent, const char *name)
    :QObject(parent, name), config(pluginConfig), m_indexCreated(false)
{
}

DocumentationPlugin::~DocumentationPlugin()
{
}

void DocumentationPlugin::autoSetup()
{
    config->setGroup("General");    
    if ( ! config->readBoolEntry("Autosetup", false) )
    {
        autoSetupPlugin();
        config->setGroup("General");
        config->writeEntry("Autosetup", true);
        config->sync();
    }
}

void DocumentationPlugin::reload()
{
    clear();
    for (QValueList<DocumentationCatalogItem *>::iterator it = catalogs.begin();
        it != catalogs.end(); ++it)
    {
        createTOC(*it);
    }
}

void DocumentationPlugin::clear()
{
    for (QValueList<DocumentationCatalogItem *>::iterator it = catalogs.begin();
        it != catalogs.end(); ++it)
    {
        clearCatalog(*it);
    }
}

void DocumentationPlugin::clearCatalog(DocumentationCatalogItem *item)
{
    //clear named catalog map
    for (QMap<QString, DocumentationCatalogItem*>::iterator it = namedCatalogs.begin();
        it != namedCatalogs.end(); ++it)
    {
        if (it.data() == item)
        {
            namedCatalogs.remove(it);
            break;
        }
    }
    //clear indexes for catalog
    QValueList<IndexItemProto *> idx = indexes[item];
    for (QValueList<IndexItemProto *>::iterator it = idx.begin(); it != idx.end(); ++it)
    {
        delete *it;
    }
    indexes.remove(item);

    //remove catalog
    catalogs.remove(item);
}

void DocumentationPlugin::createIndex(IndexBox *index)
{
    if (m_indexCreated)
        return;
    
    for (QValueList<DocumentationCatalogItem *>::iterator it = catalogs.begin();
        it != catalogs.end(); ++it)
    {
        loadIndex(index, *it);
    }
    m_indexCreated = true;
}

void DocumentationPlugin::cacheIndex(DocumentationCatalogItem *item)
{
    kdDebug() << "Creating index cache for " << item->text(0) << endl;
    
    QString cacheName = locateLocal("data", QString("kdevdocumentation/index/cache_") + item->text(0));
    QFile cacheFile(cacheName);
    if (!cacheFile.open(IO_WriteOnly))
        return;
    
    QTextStream str(&cacheFile);
    str.setEncoding(QTextStream::Unicode);
    str << CACHE_VERSION << endl;

    QValueList<IndexItemProto*> catalogIndexes = indexes[item];
    for (QValueList<IndexItemProto*>::const_iterator it = catalogIndexes.constBegin();
        it != catalogIndexes.constEnd(); ++it)
    {
        str << (*it)->text() << endl;
        str << (*it)->description() << endl;
        str << (*it)->url().url() << endl;
    }
   
    cacheFile.close();
}

bool DocumentationPlugin::loadCachedIndex(IndexBox *index, DocumentationCatalogItem *item)
{
    QString cacheName = locateLocal("data", QString("kdevdocumentation/index/cache_") + item->cacheVersion() + item->text(0));
    QFile cacheFile(cacheName);
    if (!cacheFile.open(IO_ReadOnly))
        return false;

    kdDebug() << "Using cached index for item: " << item->text(0) << endl;
    
    QTextStream str(&cacheFile);
    str.setEncoding(QTextStream::Unicode);
    QString cache = str.read();
    QStringList cacheList = QStringList::split("\n", cache, true);
    QString ver = cacheList.first();
    if (ver != CACHE_VERSION)
    {
        kdDebug() << "Wrong cache version: " << ver << endl;
        return false;
    }
    QStringList::const_iterator it = cacheList.begin();
    it++;
    QString s[3]; int c = 0;
    for (; it != cacheList.end(); ++it)
    {
        s[c] = *it;
        if (c == 2)
        {
            IndexItemProto *ii = new IndexItemProto(this, item, index, s[0], s[1]);
            ii->addURL(KURL(s[2]));
            c = 0;
        }
        else c++;
    }
    cacheFile.close();
    
    return true;
}

void DocumentationPlugin::addCatalog(DocumentationCatalogItem *item)
{
    catalogs.append(item);
    namedCatalogs[item->text(0)] = item;
//    indexes[item] = QValueList<IndexItem*>();
}

void DocumentationPlugin::addCatalogConfiguration(KListView *configurationView,
    const QString &title, const QString &url)
{
    new ConfigurationItem(configurationView, this, title, url, 
        hasCapability(Index), hasCapability(FullTextSearch));
}

void DocumentationPlugin::editCatalogConfiguration(ConfigurationItem *configurationItem,
    const QString &title, const QString &url)
{
    configurationItem->setTitle(title);
    configurationItem->setURL(url);
}

void DocumentationPlugin::deleteCatalogConfiguration(const ConfigurationItem *const configurationItem)
{
    deletedConfigurationItems << configurationItem->title();
}

void DocumentationPlugin::clearCatalogIndex(DocumentationCatalogItem *item)
{
    //clear indexes for catalog
    QValueList<IndexItemProto *> idx = indexes[item];
    for (QValueList<IndexItemProto *>::iterator it = idx.begin(); it != idx.end(); ++it)
    {
        delete *it;
    }
    indexes.remove(item);
}

void DocumentationPlugin::loadIndex(IndexBox *index, DocumentationCatalogItem *item)
{
    if (!indexEnabled(item))
        return;
    if (!needRefreshIndex(item) && loadCachedIndex(index, item))
        return;
    createIndex(index, item);
    cacheIndex(item);
}

void DocumentationPlugin::init(KListView *contents)
{
    config->setGroup("Locations");
    QMap<QString, QString> entryMap = config->entryMap("Locations");
    
    for (QMap<QString, QString>::const_iterator it = entryMap.begin();
        it != entryMap.end(); ++it)
    {
        if (catalogEnabled(it.key()))
            createCatalog(contents, it.key(), config->readPathEntry(it.key()));
    }
}

void DocumentationPlugin::reinit(KListView *contents, IndexBox *index, QStringList restrictions)
{
    config->setGroup("Locations");
    QMap<QString, QString> entryMap = config->entryMap("Locations");

    //remove deleted in configuration catalogs
    for (QStringList::const_iterator it = deletedConfigurationItems.constBegin();
        it != deletedConfigurationItems.constEnd(); ++it)
    {
        if (namedCatalogs.contains(*it))
            delete namedCatalogs[*it];
    }
    deletedConfigurationItems.clear();
    
    //update configuration
    for (QMap<QString, QString>::const_iterator it = entryMap.begin();
        it != entryMap.end(); ++it)
    {
        config->setGroup("Locations");
        if (restrictions.contains(it.key()) || (!catalogEnabled(it.key())))
        {
            if (namedCatalogs.contains(it.key()))
                delete namedCatalogs[it.key()];
        }
        else
        {
            kdDebug() << "updating 1" << endl;
            if (!namedCatalogs.contains(it.key()))    //create catalog if it does not exist
            {
                DocumentationCatalogItem * item = createCatalog(contents, it.key(), config->readPathEntry(it.key()));
                loadIndex(index, item);
                index->setDirty(true);
//                index->refill(indexes[item]);
            }
            else if (!indexEnabled(namedCatalogs[it.key()]))    //clear index if it is disabled in configuration
            {
                kdDebug() << "    updating: clearCatalogIndex" << endl;
                clearCatalogIndex(namedCatalogs[it.key()]);
            }
            else if ( (indexEnabled(namedCatalogs[it.key()]))    //index is requested in configuration but does not yet exist
                && (!indexes.contains(namedCatalogs[it.key()])) )
            {
                kdDebug() << "    index requested " << endl;
                loadIndex(index, namedCatalogs[it.key()]);
                index->setDirty(true);
            }
            m_indexCreated = true;
        }
    }
}

void DocumentationPlugin::loadCatalogConfiguration(KListView *configurationView)
{
    config->setGroup("Locations");
    QMap<QString, QString> entryMap = config->entryMap("Locations");

    for (QMap<QString, QString>::const_iterator it = entryMap.begin();
        it != entryMap.end(); ++it)
    {
        if (namedCatalogs.contains(it.key())
            && namedCatalogs[it.key()]->isProjectDocumentationItem())
            continue;
        
	config->setGroup("Locations");
        ConfigurationItem *item = new ConfigurationItem(configurationView, this, it.key(),
	    config->readPathEntry(it.key()),
            hasCapability(Index), hasCapability(FullTextSearch));
        config->setGroup("TOC Settings");
        item->setContents(config->readBoolEntry(item->title(), true));
        config->setGroup("Index Settings");
        item->setIndex(config->readBoolEntry(item->title(), false));
        config->setGroup("Search Settings");
        item->setFullTextSearch(config->readBoolEntry(item->title(), false));
    }
}

void DocumentationPlugin::saveCatalogConfiguration(KListView *configurationView)
{
    config->setGroup("Locations");
    
    for (QStringList::const_iterator it = deletedConfigurationItems.constBegin();
        it != deletedConfigurationItems.constEnd(); ++it)
    {
        config->deleteEntry(*it);
    }
    
    QListViewItemIterator it(configurationView);
    while (it.current())
    {
        ConfigurationItem *confItem = dynamic_cast<ConfigurationItem*>(it.current());
        if ( confItem->docPlugin() != this ) 
        {
            ++it;
            continue;
        }

        config->setGroup("Locations");
        if (confItem->isChanged())
            config->deleteEntry(confItem->origTitle());
        config->writePathEntry(confItem->title(), confItem->url());
        
        config->setGroup("TOC Settings");
        if (confItem->isChanged())
            config->deleteEntry(confItem->origTitle());
        config->writeEntry(confItem->title(), confItem->contents());
        
        config->setGroup("Index Settings");
        if (confItem->isChanged())
            config->deleteEntry(confItem->origTitle());
        config->writeEntry(confItem->title(), confItem->index());

        config->setGroup("Search Settings");
        if (confItem->isChanged())
            config->deleteEntry(confItem->origTitle());
        config->writeEntry(confItem->title(), confItem->fullTextSearch());

        ++it;
    }
    config->sync();
}

void DocumentationPlugin::setIndexEnabled( DocumentationCatalogItem * item, bool e )
{
    QString group = config->group();
    config->setGroup("Index Settings");
    config->writeEntry(item->text(0), e);
    config->setGroup(group);
}

bool DocumentationPlugin::indexEnabled( DocumentationCatalogItem * item ) const
{
    QString group = config->group();
    config->setGroup("Index Settings");
    bool b = config->readBoolEntry(item->text(0), false);
    config->setGroup(group);
    return b;
}

bool DocumentationPlugin::catalogEnabled(const QString &name) const
{
    QString group = config->group();
    config->setGroup("TOC Settings");
    bool b = config->readBoolEntry(name, true);
    config->setGroup(group);
    return b;
}

void DocumentationPlugin::setCatalogEnabled(const QString &name, bool e)
{
    QString group = config->group();
    config->setGroup("TOC Settings");
    config->writeEntry(name, e);
    config->setGroup(group);
}




//class IndexBox

IndexBox::IndexBox(QWidget *parent, const char *name)
    :KListBox(parent, name), m_dirty(false)
{
}

void IndexBox::addIndexItem(IndexItemProto *item)
{
    items[item->text()].append(item);
}

void IndexBox::removeIndexItem(IndexItemProto *item)
{
    QString text = item->text();
    items[text].remove(item);
    if (items[text].count() == 0)
    {
        items.remove(text);
        QListBoxItem *item = findItem(text, Qt::CaseSensitive | Qt::ExactMatch);
        if (item)
            delete item;
    }
}

void IndexBox::fill()
{
    for (QMap<QString, QValueList<IndexItemProto*> >::const_iterator it = items.begin();
        it != items.end(); ++it)
    {
        new IndexItem(this, it.key());
    }
}

void IndexBox::setDirty(bool dirty)
{
    m_dirty = dirty;
}

void IndexBox::refill()
{
    if (m_dirty)
    {
        clear();
        fill();
        setDirty(false);
    }
}


ProjectDocumentationPlugin::ProjectDocumentationPlugin(DocumentationPlugin *docPlugin, DocumentationPlugin::ProjectDocType type)
    :QObject(0, 0), m_docPlugin(docPlugin), m_catalog(0), m_type(type), m_contents(0), m_index(0)
{
    kdDebug() << "ProjectDocumentationPlugin::ProjectDocumentationPlugin for type " << type << endl;
    
    m_watch = new KDirWatch(this);
    connect(m_watch, SIGNAL(dirty(const QString&)), this, SLOT(reinit()));
    m_watch->startScan();
}

ProjectDocumentationPlugin::~ProjectDocumentationPlugin()
{
    deinit();
}

void ProjectDocumentationPlugin::init(KListView *contents, IndexBox *index, const QString &url)
{
    m_contents = contents;
    m_index = index;
    m_url = url;
    
    if (m_catalog)
        deinit();
    m_catalog = m_docPlugin->createCatalog(contents, 
        m_type == DocumentationPlugin::APIDocs ? i18n("Project API Documentation")
        : i18n("Project User Manual"), url);
    if (m_catalog)
    {
        m_catalog->setProjectDocumentationItem(true);
        m_watch->addFile(url);
    }
}

void ProjectDocumentationPlugin::reinit()
{
    deinit();
    if (m_contents != 0 && m_index != 0 && m_url != 0)
        init(m_contents, m_index, m_url);
}

void ProjectDocumentationPlugin::deinit()
{
    m_watch->removeFile(m_url);
    delete m_catalog;
    m_catalog = 0;
}

QString ProjectDocumentationPlugin::pluginName() const
{
    return m_docPlugin->pluginName();
}

QString ProjectDocumentationPlugin::catalogURL() const
{
    return m_url;
}

#include "kdevdocumentationplugin.moc"
