/****************************************************************************
 **
 ** Copyright (C) 2008-2010 Nokia Corporation and/or its subsidiary(-ies).
 ** Contact: Nokia Corporation (qt-info@nokia.com)
 **
 ** This file is part of the doxygen2qthelp project on Trolltech Labs.
 **
 ** This file may be used under the terms of the GNU General Public
 ** License version 2.0 or 3.0 as published by the Free Software Foundation
 ** and appearing in the file LICENSE.GPL included in the packaging of
 ** this file.  Please review the following information to ensure GNU
 ** General Public Licensing requirements will be met:
 ** http://www.fsf.org/licensing/licenses/info/GPLv2.html and
 ** http://www.gnu.org/copyleft/gpl.html.
 **
 ** If you are unsure which license is appropriate for your use, please
 ** contact the sales department at qt-sales@nokia.com.
 **
 ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 **
 ****************************************************************************/

#include "htmlhelpparser_p.h"
#include "simplehtmlparser_p.h"
#include "qhelpdatainterface_p.h"

#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QFile>
#include <QtCore/QHash>
#include <QtCore/QSet>

#ifdef HELP_LIB_DATA_DEBUG
# include <QDebug>
#endif

QT_BEGIN_NAMESPACE

class ContainerNode;
class LinkNode;

class HtmlHelpNode
{
public:
    virtual ~HtmlHelpNode() { }

    virtual bool isContainer() const = 0;
    virtual ContainerNode * toContainer() const = 0;
    virtual const LinkNode * toLink() const = 0;

#ifdef HELP_LIB_DATA_DEBUG
    virtual void dump(int level) const = 0;
#endif
};

class ContainerNode : public HtmlHelpNode
{
public:
    QList<HtmlHelpNode *> m_children;

    ~ContainerNode()
    {
        foreach(const HtmlHelpNode * node, m_children) {
            delete node;
        }
    }

    static void insertItem(QHelpDataIndexItem &item, QList<QHelpDataIndexItem> &dest,
        QSet<QHelpDataIndexItem> &pastEntries, QSet<QString> &pastIds);

    QHelpDataContentItem * convertAndStealChildren(
            QHelpDataContentItem * parent,
            const QString &name = QString(), const QString &href = QString());
    QList<QHelpDataIndexItem> convertAndStealChildren(int level,
            const QString &rootLabel, QSet<QHelpDataIndexItem> &pastEntries,
            QSet<QString> &pastIds);

    bool isContainer() const { return true; }
    ContainerNode * toContainer() const
    {
        return const_cast<ContainerNode *>(this);
    }
    const LinkNode * toLink() const { return NULL; }

#ifdef HELP_LIB_DATA_DEBUG
    void dump(int level = 0) const
    {
        QString indent;
        for (int i = 0; i < level; i++) {
            indent.append(" _");
        }
        foreach(const HtmlHelpNode * node, m_children) {
            Q_ASSERT(node != NULL);
            node->dump(level + 1);
        }
    }
#endif
};

class LinkNode : public HtmlHelpNode
{
public:
    QString m_name;
    QString m_subName;
    QString m_href;

    LinkNode(const QString &name, const QString &subName, const QString &href)
            : m_name(name), m_subName(subName), m_href(href) { }

    QHelpDataContentItem * convert(QHelpDataContentItem * parent) const;
    QHelpDataIndexItem convert() const;
    bool isContainer() const { return false; }
    ContainerNode * toContainer() const { return NULL; }
    const LinkNode * toLink() const { return this; }

#ifdef HELP_LIB_DATA_DEBUG
    void dump(int level = 0) const
    {
        QString indent;
        for (int i = 0; i < level; i++) {
            indent.append(" _");
        }
        qDebug() << indent << m_name << m_subName << m_href;
    }
#endif
};

QHelpDataContentItem * ContainerNode::convertAndStealChildren(
        QHelpDataContentItem * parent, const QString &name,
        const QString &href)
{
    QHelpDataContentItem * const res
            = new QHelpDataContentItem(parent, name, href);

    const int childCount = m_children.count();
    for (int i = 0; i < childCount; i++) {
        QHelpDataContentItem * item = NULL;
        HtmlHelpNode * const node = m_children[i];
        Q_ASSERT(node != NULL);
        if (node->isContainer()) {
            // No Name node before,
            item = node->toContainer()->convertAndStealChildren(res);
        } else {
            const LinkNode * const linkNode = node->toLink();
            if ((i < childCount - 1) && (m_children[i + 1]->isContainer())) {
                // Not last node, check successor
                HtmlHelpNode * const succ = m_children[i + 1];
                item = succ->toContainer()->convertAndStealChildren(
                        res, linkNode->m_name, linkNode->m_href);
                delete succ;
                // Skip container node
                i++;
            } else {
                // Successor (if existing) is a link node too
                // Delete link to the 'main page' so we don't have two of them
                if ((parent != NULL) || (i != 0)
                        || (linkNode->m_href != href)) {
                    if (!linkNode->m_href.isEmpty()) {
                        item = linkNode->convert(res);
                    }
                }
            }
        }

        delete node;
    }
    m_children.clear();

    return res;
}

static uint qHash(const QHelpDataIndexItem &key)
{
    return qHash(key.name) ^ qHash(key.reference);
}

/*static*/ void ContainerNode::insertItem(QHelpDataIndexItem &item,
        QList<QHelpDataIndexItem> &dest, QSet<QHelpDataIndexItem> &pastEntries,
        QSet<QString> &pastIds)
{
    const QString &id = item.identifier;
    const bool nameRefPairFound = pastEntries.contains(item);
    const bool idFound = pastIds.contains(id);
    if (!(nameRefPairFound && idFound)) {
        if (nameRefPairFound) {
            if (idFound) {
                // (1) Name with exact same link already there
                // (2) ID already taken
                //  \--> don't add
                Q_ASSERT(false);
            } else {
                // (1) Name with exact same link already there
                // (2) ID not taken
                //  \--> add as ("", id, ref)
                item.name = QString();
            }
        } else {
            if (idFound) {
                // (1) Name not linking to this reference yet
                // (2) ID already taken
                //  \--> add as (name, "", ref)
                item.identifier = QString();
            } else {
                // (1) Name not linking to this reference yet
                // (2) ID not taken
                //  \--> add unmodified
            }
        }
#ifdef HELP_LIB_DATA_DEBUG
        item.dump();
#endif
        pastEntries.insert(item);
        pastIds.insert(id);
        dest << item;
    }
}

QList<QHelpDataIndexItem> ContainerNode::convertAndStealChildren(
        int level, const QString &rootLabel,
        QSet<QHelpDataIndexItem> &pastEntries, QSet<QString> &pastIds)
{
    QList<QHelpDataIndexItem> res;
    QString workLabel(rootLabel);

    foreach (HtmlHelpNode * node, m_children) {
        if (node->isContainer()) {
            res += node->toContainer()->convertAndStealChildren(
                    level + 1, workLabel, pastEntries, pastIds);
        } else {
            QHelpDataIndexItem item = node->toLink()->convert();
            insertItem(item, res, pastEntries, pastIds);
            if (level == 0) {
                workLabel = item.name;
            } else {
                // -- Sample input index tree
                // clone {#1, #2}
                // * Chicken {#3}
                // * Salad {#4}
                //
                // Chicken {#3}
                // * clone {#1}
                //
                // Salad {#4}
                // * clone {#2}
                //
                // -- Created index entries
                // NAME    | ID             | REF
                // --------+----------------+--------
                // clone   | clone          | 1
                // clone   |                | 2
                // Chicken | Chicken        | 3
                //         | clone::Chicken | 3
                //         | Chicken::clone | 1
                // Salad   | Salad          | 4
                //         | clone::Salad   | 4
                //         | Salad::clone   | 2
                const QString id = rootLabel + QLatin1String("::") + item.name;
                QHelpDataIndexItem extraItem = QHelpDataIndexItem(QString(), id, item.reference);
                insertItem(extraItem, res, pastEntries, pastIds);
            }
        }
        delete node;
    }

    m_children.clear();
    return res;
}

QHelpDataContentItem * LinkNode::convert(QHelpDataContentItem * parent) const
{
    return new QHelpDataContentItem(parent, m_name, m_href);
}

QHelpDataIndexItem LinkNode::convert() const
{
    return QHelpDataIndexItem(m_name, m_name, m_href);
}

HtmlHelpParser::HtmlHelpParser()
        : m_state(WAIT_NAME_OR_LOCAL),
        m_tree(new ContainerNode)
{
    m_nodeStack.push(m_tree);
}

HtmlHelpParser::~HtmlHelpParser()
{
    delete m_tree;
}

void HtmlHelpParser::parseFile(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        return;
    }

    QString content;
    while (!file.atEnd()) {
        content.append(file.readLine());
    }
    file.close();

    parseString(content);
}

void HtmlHelpParser::parseString(const QString &text)
{
    SimpleHtmlParser parser;
    parser.parse(text, this);
}

const QList<QHelpDataIndexItem> HtmlHelpParser::stealIndex()
{
    QSet<QHelpDataIndexItem> noDupEntries;
    QSet<QString> noDupIds;
    return m_tree->convertAndStealChildren(0, QString(),
            noDupEntries, noDupIds);
}

const QList<QHelpDataContentItem*> HtmlHelpParser::stealToc(
        const QString &rootName, const QString &rootHref)
{
    Q_ASSERT(m_tree != NULL);
    QHelpDataContentItem * const convertedTree
            = m_tree->convertAndStealChildren(NULL, rootName, rootHref);
    Q_ASSERT(convertedTree != NULL);
#ifdef HELP_LIB_DATA_DEBUG
    convertedTree->dump();
#endif
    QList<QHelpDataContentItem*> res;
    res.append(convertedTree);
    return res;
}

void HtmlHelpParser::onTagOpen(const QString &tagName,
        const QStringList &attributes)
{
#ifdef HELP_LIB_DATA_DEBUG
    qDebug() << "OPEN" << tagName << "ATTS" << attributes;
#endif
    if (tagName == QString("ul")) {
        ContainerNode * const top = m_nodeStack.top();
        Q_ASSERT(top != NULL);
        if (!((top == m_tree) && (m_tree->m_children.isEmpty()))) {
            ContainerNode * const node = new ContainerNode;
            Q_ASSERT(node != NULL);
            top->m_children.append(node);
            m_nodeStack.push(node);
        }
    } else if (tagName == QString("param")) {
        const QString name = findAttribute(attributes, "name");
        if (name.isEmpty()) {
            return;
        }
        const QString value = findAttribute(attributes, "value");
        if (value.isEmpty()) {
            return;
        }

        if (name == QString("Name")) {
            switch (m_state) {
            case WAIT_NAME_OR_LOCAL:
                // First entry is a name, nothing fixed yet
                m_name = value;
                m_state = WAIT_NAME_NAME_OR_NAME_LOCAL;
                break;

            case WAIT_NAME_NAME_OR_NAME_LOCAL:
                // Second entry is a name too -> must be a list
                m_subName = value;
                m_state = WAIT_NAME_NAME_LOCAL;
                break;

            case WAIT_NAME_NAME_LOCAL_NAME:
                m_subName = value;
                m_state = WAIT_NAME_NAME_LOCAL;
                break;

            case WAIT_LOCAL_NAME:
                { ContainerNode * const top = m_nodeStack.top();
                Q_ASSERT(top != NULL);
                top->m_children.append(new LinkNode(value, "", m_href)); }
                m_state = WAIT_NAME_OR_LOCAL;
                break;

            default:
                ; // NOOP
            }
        } else if (name == QString("Local")) {
            switch (m_state) {
            case WAIT_NAME_OR_LOCAL:
                // First entry is a local -> must be a single link, name follows
                m_href = value;
                m_state = WAIT_LOCAL_NAME;
                break;

            case WAIT_NAME_NAME_OR_NAME_LOCAL:
                // Second entry is a href -> must be a single link
                { ContainerNode * const top = m_nodeStack.top();
                Q_ASSERT(top != NULL);
                top->m_children.append(new LinkNode(m_name, "", value)); }

                m_state = WAIT_NAME_OR_LOCAL;
                break;

            case WAIT_NAME_NAME_LOCAL:
                { ContainerNode * const top = m_nodeStack.top();
                Q_ASSERT(top != NULL);
                top->m_children.append(new LinkNode(m_name, m_subName, value)); }

                m_state = WAIT_NAME_NAME_LOCAL_NAME;
                break;

            default:
                ; // NOOP
            }
        }
    }
}

void HtmlHelpParser::onTagClose(const QString &tagName)
{
#ifdef HELP_LIB_DATA_DEBUG
    qDebug() << "CLOSE" << tagName;
#endif
    if (tagName == QString("ul")) {
        if (m_nodeStack.size() > 1) {
            m_nodeStack.pop();
        }
    } else if (tagName == QString("object")) {
        switch (m_state) {
        case WAIT_NAME_NAME_OR_NAME_LOCAL:
            // Name only, no href
            { ContainerNode * const top = m_nodeStack.top();
            Q_ASSERT(top != NULL);
            top->m_children.append(new LinkNode(m_name, "", "")); }
            break;

        default:
            ; // NOOP
        }

        m_state = WAIT_NAME_OR_LOCAL;
        m_name.clear();
        m_subName.clear();
    }
}

void HtmlHelpParser::onTextChunk(const QString & /*text*/)
{
    // NOOP
}

void HtmlHelpParser::onStop()
{
    m_nodeStack.clear();
#ifdef HELP_LIB_DATA_DEBUG
    m_tree->dump();
#endif
}

/*static*/ QString HtmlHelpParser::findAttribute(const QStringList &list,
        const QString &key)
{
    const int count = list.count();
    for (int i = 0; i < count - 1; i += 2) {
        if (list.at(i) == key) {
            return list.at(i + 1);
        }
    }
    return QString();
}

QT_END_NAMESPACE
