/*
   This file is part of the KDE project
   Copyright (C) 2001, 2002, 2004 Nicolas GOUTTE <goutte@kde.org>

   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., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include <QString>
#include <QTextCodec>
#include <QFile>

#include <klocale.h>
#include <kdebug.h>

#include <KoPageFormat.h>
#include <KWEFBaseWorker.h>

#include "ExportFilter.h"
#include "ExportCss.h"

QString HtmlCssWorker::escapeCssIdentifier(const QString& strText) const
{
    // Reference: section 4.1.3 of the CSS2 recommendation
    // However most HTML user agents support this section only in a restrictive way, so we cannot use any CSS escape

    // NOTE: we do not guarantee anymore that the style name is unique! (### FIXME)

    QString strReturn;

    // Taken in the restrictive way, an identifier can only start with a letter.
    const QChar qch0(strText[0]);
    if ((qch0 < 'a' || qch0 > 'z') && (qch0 < 'A' || qch0 > 'Z')) {
        // Not a letter, so we have to add a prefix
        strReturn += "kWoRd_"; // The curious spelling is for allowing a HTML import to identfy it and to remove it.
        // The processing of the character itself is done below
    }

    for (int i = 0; i < strText.length(); i++) {
        const QChar qch(strText.at(i));
        const ushort ch = qch.unicode();

        if (((ch >= 'a') && (ch <= 'z'))
                || ((ch >= 'A') && (ch <= 'Z'))
                || ((ch >= '0') && (ch <= '9'))
                || (ch == '-') || (ch == '_')) { // The underscore is allowed by the CSS2 errata
            // Normal allowed characters (without any problem)
            strReturn += qch;
        } else if ((ch <= ' ') || (ch >= 128 && ch <= 160)) { // space (breaking or not) and control characters
            // CSS2 would allow to escape it but not any HTML user agent supports this
            strReturn += '_';
        } else if ((ch >= 161) && (getCodec()->canEncode(qch))) {
            // Any Unicode character greater or egual to 161 is allowed
            // except if it cannot be written in the encoding
            strReturn += qch;
        } else { // if ch >= 33 && ch <=127 with holes (or not in encoding)
            // Either CSS2 does not allow this character unescaped or it is not in the encoding
            // but a CSS escape would break some HTML user agents (e.g. Mozilla 1.4)
            // So we have to do our own incompatible cooking. :-(
            strReturn += "--"; // start our private escape
            strReturn += QString::number(ch, 16);
            strReturn += "--"; // end our private escape
        }
    }
    return strReturn;
}

QString HtmlCssWorker::textFormatToCss(const TextFormatting& formatOrigin,
                                       const TextFormatting& formatData, const bool force) const
{
    // TODO: as this method comes from the AbiWord filter,
    // TODO:   verify that it is working for HTML

    // TODO: rename variable formatData
    QString strElement; // TODO: rename this variable

    // Font name
    QString fontName = formatData.fontName;
    if (!fontName.isEmpty()
            && (force || (formatOrigin.fontName != formatData.fontName))) {
        strElement += "font-family: ";
        if (fontName.find(' ') == -1)
            strElement += escapeHtmlText(fontName);
        else {  // If the font name contains a space, it should be quoted.
            strElement += '\'';
            strElement += escapeHtmlText(fontName);
            strElement += '\'';
        }
        // ### TODO: add alternative font names
        strElement += "; ";
    }

    if (force || (formatOrigin.italic != formatData.italic)) {
        // Font style
        strElement += "font-style: ";
        if (formatData.italic) {
            strElement += "italic";
        } else {
            strElement += "normal";
        }
        strElement += "; ";
    }

    if (force || ((formatOrigin.weight >= 75) != (formatData.weight >= 75))) {
        strElement += "font-weight: ";
        if (formatData.weight >= 75) {
            strElement += "bold";
        } else {
            strElement += "normal";
        }
        strElement += "; ";
    }

    if (force || (formatOrigin.fontSize != formatData.fontSize)) {
        const int size = formatData.fontSize;
        if (size > 0) {
            // We use absolute font sizes.
            strElement += "font-size: ";
            strElement += QString::number(size, 10);
            strElement += "pt; ";
        }
    }

    if (force || (formatOrigin.fgColor != formatData.fgColor)) {
        if (formatData.fgColor.isValid()) {
            // Give color
            strElement += "color: ";
            strElement += formatData.fgColor.name();
            strElement += "; ";
        }
    }

    if (force || (formatOrigin.bgColor != formatData.bgColor)) {
        if (formatData.bgColor.isValid()) {
            // Give background color
            strElement += "background-color: ";
            strElement += formatData.bgColor.name();
            strElement += "; ";
        }
    }

    if (force || (formatOrigin.underline != formatData.underline)
            || (formatOrigin.strikeout != formatData.strikeout)) {
        strElement += "text-decoration: ";
        if (formatData.underline) {
            strElement += "underline";
        } else if (formatData.strikeout) {
            strElement += "line-through";
        } else {
            strElement += "none";
        }
        strElement += "; ";
    }

    if (force || (formatOrigin.fontAttribute != formatData.fontAttribute)) {
        bool smallcaps = false;
        strElement += "text-transform: ";
        if (formatData.fontAttribute == "uppercase") {
            strElement += "uppercase";
        } else if (formatData.fontAttribute == "lowercase") {
            strElement += "lowercase";
        } else if (formatData.fontAttribute == "smallcaps") {
            strElement += "none";
            smallcaps = true;
        } else {
            strElement += "none";
        }
        strElement += "; ";
        // ### TODO: mostly issuing font-variant is not necessary.
        strElement += "font-variant:";
        if (smallcaps)
            strElement += "small-caps";
        else
            strElement += "normal";
        strElement += "; ";
    }

    // TODO: As this is the last property, do not put a semi-colon

    return strElement;
}

QString HtmlCssWorker::getStartOfListOpeningTag(const CounterData::Style typeList, bool& ordered)
{
    QString strResult;
    switch (typeList) {
    case CounterData::STYLE_CUSTOMBULLET: // We cannot keep the custom type/style
    default: {
        ordered = false;
        strResult = "<ul>\n";
        break;
    }
    case CounterData::STYLE_NONE: {
        ordered = false;
        strResult = "<ul style=\"list-style-type:none\">\n";
        break;
    }
    case CounterData::STYLE_CIRCLEBULLET: {
        ordered = false;
        strResult = "<ul style=\"list-style-type:circle\">\n";
        break;
    }
    case CounterData::STYLE_SQUAREBULLET: {
        ordered = false;
        strResult = "<ul style=\"list-style-type:square\">\n";
        break;
    }
    case CounterData::STYLE_DISCBULLET: {
        ordered = false;
        strResult = "<ul style=\"list-style-type:disc\">\n";
        break;
    }
    case CounterData::STYLE_NUM: {
        ordered = true;
        strResult = "<ol style=\"list-style-type:decimal\">\n";
        break;
    }
    case CounterData::STYLE_ALPHAB_L: {
        ordered = true;
        strResult = "<ol style=\"list-style-type:lower-alpha\">\n";
        break;
    }
    case CounterData::STYLE_ALPHAB_U: {
        ordered = true;
        strResult = "<ol style=\"list-style-type:upper-alpha\">\n";
        break;
    }
    case CounterData::STYLE_ROM_NUM_L: {
        ordered = true;
        strResult = "<ol style=\"list-style-type:lower-roman\">\n";
        break;
    }
    case CounterData::STYLE_ROM_NUM_U: {
        ordered = true;
        strResult = "<ol style=\"list-style-type:upper-roman\">\n";
        break;
    }
    case CounterData::STYLE_CUSTOM: {
        // We cannot keep the custom type/style
        ordered = true;
        strResult = "<ol>\n";
        break;
    }
    }
    return strResult;
}

QString HtmlCssWorker::layoutToCss(const LayoutData& layoutOrigin,
                                   const LayoutData& layout, const bool force) const
{
    QString strLayout;

    if (force || (layoutOrigin.alignment != layout.alignment)) {
        if ((layout.alignment == "left") || (layout.alignment == "right")
                || (layout.alignment == "center") || (layout.alignment == "justify")) {
            strLayout += QString("text-align:%1; ").arg(layout.alignment);
        } else if (layout.alignment == "auto") {
            // Do nothing, the user-agent should be more intelligent than us.
        } else {
            kWarning(30503) << "Unknown alignment: " << layout.alignment;
        }
    }

    if ((layout.indentLeft >= 0.0)
            && (force || (layoutOrigin.indentLeft != layout.indentLeft))) {
        strLayout += QString("margin-left:%1pt; ").arg(layout.indentLeft);
    }

    if ((layout.indentRight >= 0.0)
            && (force || (layoutOrigin.indentRight != layout.indentRight))) {
        strLayout += QString("margin-right:%1pt; ").arg(layout.indentRight);
    }

    if (force || (layoutOrigin.indentLeft != layout.indentLeft)) {
        strLayout += QString("text-indent:%1pt; ").arg(layout.indentFirst);
    }

    if ((layout.marginBottom >= 0.0)
            && (force || (layoutOrigin.marginBottom != layout.marginBottom))) {
        strLayout += QString("margin-bottom:%1pt; ").arg(layout.marginBottom);
    }

    if ((layout.marginTop >= 0.0)
            && (force || (layoutOrigin.marginTop != layout.marginTop))) {
        strLayout += QString("margin-top:%1pt; ").arg(layout.marginTop);
    }

    if (force
            || (layoutOrigin.lineSpacingType != layout.lineSpacingType)
            || (layoutOrigin.lineSpacing != layout.lineSpacing)) {
        switch (layout.lineSpacingType) {
        case LayoutData::LS_CUSTOM: {
            // ### TODO: CSS 2 does not known "at-least".
#if 0
            // We have a custom line spacing (in points)
            const QString height(QString::number(layout.lineSpacing));    // ### TODO: rounding?
            strLayout += "style:line-spacing:";
            strLayout += height;
            strLayout += "pt; ";
#endif
            break;
        }
        case LayoutData::LS_SINGLE: {
            strLayout += "line-height:normal; "; // One
            break;
        }
        case LayoutData::LS_ONEANDHALF: {
            strLayout += "line-height:150%; "; // One-and-half
            break;
        }
        case LayoutData::LS_DOUBLE: {
            strLayout += "line-height:200%; "; // Two
            break;
        }
        case LayoutData::LS_MULTIPLE: {
            const QString mult(QString::number(qRound(layout.lineSpacing * 100)));
            strLayout += "line-height:";
            strLayout += mult;
            strLayout += "%; ";
            break;
        }
        case LayoutData::LS_FIXED: {
            // We have a fixed line height (in points)
            const QString height(QString::number(layout.lineSpacing));    // ### TODO: rounding?
            strLayout += "line-height:";
            strLayout += height;
            strLayout += "pt; ";
            break;
        }
        case LayoutData::LS_ATLEAST: {
            // ### TODO: CSS 2 does not known "at-least".
            // ### TODO:  however draft CCS3 (module 'line') has 'line-stacking-strategy' to tweak this behaviour
            // We have a at-least line height (in points)
            const QString height(QString::number(layout.lineSpacing));    // ### TODO: rounding?
            strLayout += "line-height:";
            strLayout += height;
            strLayout += "pt; ";
            break;
        }
        default: {
            kWarning(30503) << "Unsupported lineSpacingType: " << layout.lineSpacingType << " (Ignoring!)";
            break;
        }
        }
    }

    // TODO: Konqueror/KHTML does not support "text-shadow"
    if (!force
            && (layoutOrigin.shadowDirection == layout.shadowDirection)
            && (layoutOrigin.shadowDistance == layout.shadowDistance)) {
        // Do nothing!
    } else if ((!layout.shadowDirection) || (!layout.shadowDistance)) {
        strLayout += "text-shadow:";
        strLayout += "none; ";
    } else {
        double xDistance, yDistance;
        const double distance = layout.shadowDistance;
        switch (layout.shadowDirection) {
        case 1: { // SD_LEFT_UP
            xDistance = (-distance);
            yDistance = (-distance);
            break;
        }
        case 2: { // SD_UP
            xDistance = 0;
            yDistance = (-distance);
            break;
        }
        case 3: { // SD_RIGHT_UP
            xDistance = (distance);
            yDistance = (-distance);
            break;
        }
        case 4: { // SD_RIGHT
            xDistance = (distance);
            yDistance = 0;
            break;
        }
        case 5: { // SD_RIGHT_BOTTOM
            xDistance = (distance);
            yDistance = (distance);
            break;
        }
        case 6: { // SD_BOTTOM
            xDistance = 0;
            yDistance = (distance);
            break;
        }
        case 7: { // SD_LEFT_BOTTOM
            xDistance = (-distance);
            yDistance = (distance);
            break;
        }
        case 8: { // SD_LEFT
            xDistance = (distance);
            yDistance = 0;
            break;
        }
        default: {
            xDistance = 0;
            yDistance = 0;
            break;
        }
        }
        if ((!xDistance) && (!yDistance)) {
            strLayout += "text-shadow:";
            strLayout += "none; ";
        } else {
            strLayout += "text-shadow:";
            strLayout += QString("%1 %2pt %3pt; ").arg(layout.shadowColor.name())
                         .arg(xDistance, 0, 'f', 0).arg(yDistance, 0, 'f', 0);
            // We do not want any scientific notation or any decimal
        }
    }

    // TODO: borders

    // This must remain last, as the last property does not have a semi-colon
    strLayout += textFormatToCss(layoutOrigin.formatData.text,
                                 layout.formatData.text, force);

    return strLayout;
}

void HtmlCssWorker::openParagraph(const QString& strTag,
                                  const LayoutData& layout, QChar::Direction direction)
{
    const LayoutData& styleLayout = m_styleMap[layout.styleName];

    *m_streamOut << '<' << strTag;

    // Opening elements
    *m_streamOut << " class=\"" << escapeCssIdentifier(layout.styleName);
    *m_streamOut << "\"";

    QString strStyle = layoutToCss(styleLayout, layout, false);
    if (!strStyle.isEmpty()) {
        *m_streamOut << " style=\"" << strStyle;
        if (direction == QChar::DirRLE) {
            *m_streamOut << "direction: rtl; unicode-bidi: embed; ";
        } else if (direction == QChar::DirRLO) {
            *m_streamOut << "direction: rtl; unicode-bidi: override; ";
        }
        *m_streamOut << "\"";
    }

    *m_streamOut << ">";

    if (1 == layout.formatData.text.verticalAlignment) {
        *m_streamOut << "<sub>"; //Subscript
    } else if (2 == layout.formatData.text.verticalAlignment) {
        *m_streamOut << "<sup>"; //Superscript
    }
    if (layout.alignment == "center") *m_streamOut << "<center>";
}

void HtmlCssWorker::closeParagraph(const QString& strTag,
                                   const LayoutData& layout)
{
    if (2 == layout.formatData.text.verticalAlignment) {
        *m_streamOut << "</sup>"; //Superscript
    } else if (1 == layout.formatData.text.verticalAlignment) {
        *m_streamOut << "</sub>"; //Subscript
    }

    if (layout.alignment == "center") *m_streamOut << "</center>";
    *m_streamOut << "</" << strTag << ">\n";
}

void HtmlCssWorker::openSpan(const FormatData& formatOrigin, const FormatData& format)
{
    *m_streamOut << "<span style=\"";
    *m_streamOut << textFormatToCss(formatOrigin.text, format.text, false);
    *m_streamOut << "\">"; // close span opening tag

    if (1 == format.text.verticalAlignment) {
        *m_streamOut << "<sub>"; //Subscript
    } else if (2 == format.text.verticalAlignment) {
        *m_streamOut << "<sup>"; //Superscript
    }
}

void HtmlCssWorker::closeSpan(const FormatData& formatOrigin, const FormatData& format)
{
    Q_UNUSED(formatOrigin);
    if (2 == format.text.verticalAlignment) {
        *m_streamOut << "</sup>"; //Superscript
    } else if (1 == format.text.verticalAlignment) {
        *m_streamOut << "</sub>"; //Subscript
    }

    *m_streamOut << "</span>";
}

bool HtmlCssWorker::doFullPaperFormat(const int f,
                                      const double width, const double height, const int o)
{
    KoPageFormat::Format format = static_cast<KoPageFormat::Format>(f);
    KoPageFormat::Orientation orientation = static_cast<KoPageFormat::Orientation>(o);

    double definedWidth = KoPageFormat::width(format, orientation);
    double definedHeight = KoPageFormat::height(format, orientation);

    if (definedHeight < 0 || definedWidth < 0) {
        definedWidth = width;
        definedHeight = height;
    }

    m_strPageSize = "size: ";
    m_strPageSize += QString::number(width);
    m_strPageSize += "pt ";
    m_strPageSize += QString::number(height);
    m_strPageSize += "pt;";
    return true;
}

bool HtmlCssWorker::doFullPaperBorders(const double top, const double left,
                                       const double bottom, const double right)
{
    m_strPaperBorders = "  margin-top: ";
    m_strPaperBorders += QString::number(top);
    m_strPaperBorders += "pt;\n";
    m_strPaperBorders += "  margin-left: ";
    m_strPaperBorders += QString::number(left);
    m_strPaperBorders += "pt;\n";
    m_strPaperBorders += "  margin-bottom: ";
    m_strPaperBorders += QString::number(bottom);
    m_strPaperBorders += "pt;\n";
    m_strPaperBorders += "  margin-right: ";
    m_strPaperBorders += QString::number(right);
    m_strPaperBorders += "pt;\n";

    return true;
}

bool HtmlCssWorker::doOpenStyles(void)
{
    *m_streamOut << "<style type=\"text/css\">\n";
    if (!isXML()) {
        // Put the style under comment to increase the compatibility with old browsers
        // However in XHTML 1.0, you cannot put the style definition into HTML comments
        *m_streamOut << "<!--\n";
    }

    // Say who we are (with the CVS revision number)
    const QString strVersion("$Revision: 1061376 $");
    // Eliminate the dollar signs
    //  (We don't want that the version number changes if the HTML file is itself put in a CVS storage.)
    *m_streamOut << "/* KWORD_CSS_EXPORT ="
    << strVersion.mid(10).remove('$')
    << "*/\n";

    // TODO: does KWord give a paper color?
    *m_streamOut << "BODY\n{\n  background-color: #FFFFFF\n}\n";

    return true;
}

bool HtmlCssWorker::doFullDefineStyle(LayoutData& layout)
{
    //Register style in the style map
    m_styleMap[layout.styleName] = layout;

    // We do not limit (anymore) any style to <h1> ... <h6>, because
    //   the style could be forced on <p> by the layout.

    *m_streamOut << "." << escapeCssIdentifier(layout.styleName);
    *m_streamOut << "\n{\n  " << layoutToCss(layout, layout, true) << "\n}\n";

    return true;
}

bool HtmlCssWorker::doCloseStyles(void)
{
    if (!m_strPageSize.isEmpty()) {
        *m_streamOut << "@page\n{\n  ";
        *m_streamOut << m_strPageSize;
        *m_streamOut << "\n";
        *m_streamOut << m_strPaperBorders; // ends with a LF
        *m_streamOut << "}\n";
    }

    if (!isXML()) {
        // Put the style under comment to increase the compatibility with old browsers
        // However in XHTML 1.0, you cannot put the style definition into HTML comments
        *m_streamOut << "-->\n";
    }
    *m_streamOut << "</style>\n";

    return true;
}

