/* This file is part of the KDE project
   SPDX-FileCopyrightText: 2006 Martin Pfeiffer <hubipete@gmx.net>

   SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "AttributeManager.h"

#include "BasicElement.h"
#include "ElementFactory.h"
#include "FormulaDebug.h"

#include <KoPostscriptPaintDevice.h>
#include <KoUnit.h>
#include <KoViewConverter.h>

#include <QColor>
#include <QFontMetricsF>
#include <QRegularExpression>

//// Copied from calligra KoUnit.h
//
//// 1 inch ^= 72 pt
//// 1 inch ^= 25.399956 mm (-pedantic ;p)
//// 1 pt = 1/12 pi
//// 1 pt ^= 0.0077880997 cc
//// 1 cc = 12 dd
//// Note: I don't use division but multiplication with the inverse value
//// because it's faster ;p (Werner)
// #define POINT_TO_MM(px) ((px)*0.352777167)
// #define MM_TO_POINT(mm) ((mm)*2.83465058)
// #define POINT_TO_CM(px) ((px)*0.0352777167)
// #define CM_TO_POINT(cm) ((cm)*28.3465058)
// #define POINT_TO_DM(px) ((px)*0.00352777167)
// #define DM_TO_POINT(dm) ((dm)*283.465058)
// #define POINT_TO_INCH(px) ((px)*0.01388888888889)
// #define INCH_TO_POINT(inch) ((inch)*72.0)
// #define MM_TO_INCH(mm) ((mm)*0.039370147)
// #define INCH_TO_MM(inch) ((inch)*25.399956)
// #define POINT_TO_PI(px)((px)*0.083333333)
// #define POINT_TO_CC(px)((px)*0.077880997)
// #define PI_TO_POINT(pi)((pi)*12)
// #define CC_TO_POINT(cc)((cc)*12.840103)

AttributeManager::AttributeManager()
{
    m_viewConverter = nullptr;
}

AttributeManager::~AttributeManager() = default;

QString AttributeManager::findValue(const QString &attribute, const BasicElement *element) const
{
    // check if the current element has a value assigned
    QString value = element->attribute(attribute);
    if (!value.isEmpty()) {
        //         debugFormula << "checking for attribute "<<attribute <<" returning (s)"<<value;
        return value;
    }
    // if not, check if any of the parent elements inherits a value
    BasicElement *tmpParent = element->parentElement();
    while (tmpParent) {
        value = tmpParent->inheritsAttribute(attribute);
        if (!value.isEmpty()) {
            //             debugFormula << "checking for attribute "<<attribute <<" returning (p)"<<value;
            return value;
        } else {
            tmpParent = tmpParent->parentElement();
        }
    }

    // if not, return the default value of the attribute
    //     debugFormula << "checking for attribute "<<attribute <<" returning (d) "<<element->attributesDefaultValue( attribute );
    return element->attributesDefaultValue(attribute);
}

bool AttributeManager::boolOf(const QString &attribute, const BasicElement *element) const
{
    return findValue(attribute, element) == "true";
}

qreal AttributeManager::doubleOf(const QString &attribute, const BasicElement *element) const
{
    return lengthToPixels(parseUnit(findValue(attribute, element), element), element, attribute);
}

QList<qreal> AttributeManager::doubleListOf(const QString &attribute, const BasicElement *element) const
{
    QList<qreal> doubleList;
    QStringList tmp = findValue(attribute, element).split(' ');
    foreach (const QString &doubleValue, tmp)
        doubleList << lengthToPixels(parseUnit(doubleValue, element), element, attribute);

    return doubleList;
}

QString AttributeManager::stringOf(const QString &attribute, const BasicElement *element) const
{
    return findValue(attribute, element);
}

QColor AttributeManager::colorOf(const QString &attribute, const BasicElement *element) const
{
    QString tmpColor = findValue(attribute, element);
    if (attribute == "mathbackground" && tmpColor.isEmpty())
        return Qt::transparent;

    return QColor(tmpColor);
}

Align AttributeManager::alignOf(const QString &attribute, const BasicElement *element) const
{
    return parseAlign(findValue(attribute, element));
}

QList<Align> AttributeManager::alignListOf(const QString &attribute, const BasicElement *element) const
{
    QList<Align> alignList;
    QStringList tmpList = findValue(attribute, element).split(' ');

    foreach (const QString &tmp, tmpList)
        alignList << parseAlign(tmp);

    return alignList;
}

Qt::PenStyle AttributeManager::penStyleOf(const QString &attribute, const BasicElement *element) const
{
    return parsePenStyle(findValue(attribute, element));
}

QList<Qt::PenStyle> AttributeManager::penStyleListOf(const QString &attribute, const BasicElement *element) const
{
    QList<Qt::PenStyle> penStyleList;
    QStringList tmpList = findValue(attribute, element).split(' ');

    foreach (const QString &tmp, tmpList)
        penStyleList << parsePenStyle(tmp);

    return penStyleList;
}

int AttributeManager::scriptLevel(const BasicElement *parent, int index) const
{
    ElementType parentType = parent->elementType();
    int current_scaleLevel = parent->scaleLevel();

    /** First check for types where all children are scaled */
    switch (parentType) {
    case Fraction:
        if (parent->displayStyle() == false)
            return current_scaleLevel + 1;
        else
            return current_scaleLevel;
    case Style: {
        QString tmp = parent->attribute("scriptlevel");
        if (tmp.startsWith('+'))
            return current_scaleLevel + tmp.remove(0, 1).toInt();
        if (tmp.startsWith('-'))
            return current_scaleLevel - tmp.remove(0, 1).toInt();
        return tmp.toInt();
    }
    case MultiScript:
        return current_scaleLevel + 1;
    case Table:
        return current_scaleLevel + 1;
    default:
        break;
    }
    if (index == 0)
        return current_scaleLevel;
    /** Now check for types where the first child isn't scaled, but the rest are */
    switch (parentType) {
    case SubScript:
    case SupScript:
    case SubSupScript:
        return current_scaleLevel + 1;
    case Under:
        if (boolOf("accentunder", parent))
            return current_scaleLevel + 1;
        else
            return current_scaleLevel;
    case Over:
        if (boolOf("accent", parent))
            return current_scaleLevel + 1;
        else
            return current_scaleLevel;
    case UnderOver:
        if ((index == 1 && boolOf("accentunder", parent)) || (index == 2 && boolOf("accent", parent)))
            return current_scaleLevel + 1;
        else
            return current_scaleLevel;
    case Root:
        /* second argument to root is the base */
        return current_scaleLevel + 1;
    default:
        return current_scaleLevel;
    }
}

qreal AttributeManager::lineThickness(const BasicElement *element) const
{
    QFontMetricsF fm(font(element));
    return fm.height() * 0.06;
}

qreal AttributeManager::layoutSpacing(const BasicElement *element) const
{
    QFontMetricsF fm(font(element));
    //    return fm.height() * 0.166667 ;
    return fm.height() * 0.05;
}

qreal AttributeManager::lengthToPixels(Length length, const BasicElement *element, const QString &attribute) const
{
    if (length.value == 0)
        return 0;

    switch (length.unit) {
    case Length::Em: {
        QFontMetricsF fm(font(element));
        return fm.height() * length.value;
    }
    case Length::Ex: {
        QFontMetricsF fm(font(element));
        return fm.xHeight() * length.value;
    }
    case Length::Percentage:
        return lengthToPixels(parseUnit(element->attributesDefaultValue(attribute), element), element, attribute) * length.value / 100.0;
    case Length::Px: // pixels
        return length.value;
    case Length::In: /* Note for the units below we assume point == pixel.  */
        return INCH_TO_POINT(length.value);
    case Length::Cm:
        return CM_TO_POINT(length.value);
    case Length::Mm:
        return MM_TO_POINT(length.value);
    case Length::Pt:
        return length.value;
    case Length::Pc:
        return PI_TO_POINT(length.value);
    case Length::None:
    default:
        return length.value;
    }
}

Length AttributeManager::parseUnit(const QString &value, const BasicElement *element) const
{
    Q_UNUSED(element)
    Length length;

    if (value.isEmpty())
        return length;
    static QRegularExpression re("(-?[\\d\\.]*) *(px|em|ex|in|cm|pc|mm|pt|%)?", QRegularExpression::CaseInsensitiveOption);
    QRegularExpressionMatch match;
    if (value.indexOf(re, 0, &match) == -1)
        return length;
    QString real = match.captured(1);
    QString unit = match.captured(2).toLower();

    bool ok;
    qreal number = real.toDouble(&ok);
    if (!ok)
        return length;

    length.value = number;
    if (!unit.isEmpty()) {
        if (unit == "em") {
            length.unit = Length::Mm;
            length.type = Length::Relative;
        } else if (unit == "ex") {
            length.unit = Length::Ex;
            length.type = Length::Relative;
        } else if (unit == "px") {
            length.unit = Length::Px;
            length.type = Length::Pixel;
        } else if (unit == "in") {
            length.unit = Length::In;
            length.type = Length::Absolute;
        } else if (unit == "cm") {
            length.unit = Length::Cm;
            length.type = Length::Absolute;
        } else if (unit == "mm") {
            length.unit = Length::Mm;
            length.type = Length::Absolute;
        } else if (unit == "pt") {
            length.unit = Length::Pt;
            length.type = Length::Relative;
        } else if (unit == "pc") {
            length.unit = Length::Pc;
            length.type = Length::Relative;
        } else if (unit == "%") {
            length.unit = Length::Percentage;
            length.type = Length::Relative;
        } else {
            length.unit = Length::None;
            length.type = Length::NoType;
        }
    }

    return length;
}

Align AttributeManager::parseAlign(const QString &value) const
{
    if (value == "right")
        return Right;
    else if (value == "left")
        return Left;
    else if (value == "center")
        return Center;
    else if (value == "top")
        return Top;
    else if (value == "bottom")
        return Bottom;
    else if (value == "baseline")
        return BaseLine;
    else if (value == "axis")
        return Axis;
    else
        return InvalidAlign;
}

Qt::PenStyle AttributeManager::parsePenStyle(const QString &value) const
{
    if (value == "solid")
        return Qt::SolidLine;
    else if (value == "dashed")
        return Qt::DashLine;
    else
        return Qt::NoPen;
}

QFont AttributeManager::font(const BasicElement *element) const
{
    // TODO process the mathvariant values partly
    // normal -> do nothing.
    // if contains bold -> font.setBold( true )
    // if contains italic -> font.setItalic( true )
    // if contains sans-serif setStyleHint( SansSerif ) --> Helvetica

    QFont font;
    Length unit = parseUnit(findValue("fontsize", element), element);
    if (unit.type == Length::Absolute) {
        font.setPointSizeF(lengthToPixels(unit, element, "fontsize"));
    } else if (unit.type == Length::Relative) {
        font.setPointSizeF(lengthToPixels(unit, element, "fontsize") * element->scaleFactor());
    } else if (unit.type == Length::Pixel) {
        font.setPixelSize(lengthToPixels(unit, element, "fontsize") * element->scaleFactor());
    }
    return font;
}

void AttributeManager::setViewConverter(KoViewConverter *converter)
{
    m_viewConverter = converter;
}

qreal AttributeManager::maxHeightOfChildren(const BasicElement *element) const
{
    qreal maxHeight = 0.0;
    foreach (BasicElement *tmp, element->childElements())
        maxHeight = qMax(maxHeight, tmp->height());

    return maxHeight;
}

qreal AttributeManager::maxWidthOfChildren(const BasicElement *element) const
{
    qreal maxWidth = 0.0;
    foreach (BasicElement *tmp, element->childElements())
        maxWidth = qMax(maxWidth, tmp->width());

    return maxWidth;
}
qreal AttributeManager::parseMathSpace(const QString &value, const BasicElement *element) const
{
    QFontMetricsF fm(font(element));
    qreal conversionEmToPixels = fm.xHeight();

    if (value == "negativeveryverythinmathspace")
        return -1 * conversionEmToPixels * 0.055556;
    else if (value == "negativeverythinmathspace")
        return -1 * conversionEmToPixels * 0.111111;
    else if (value == "negativethinmathspace")
        return -1 * conversionEmToPixels * 0.166667;
    else if (value == "negativemediummathspace")
        return -1 * conversionEmToPixels * 0.222222;
    else if (value == "negativethickmathspace")
        return -1 * conversionEmToPixels * 0.277778;
    else if (value == "negativeverythickmathspace")
        return -1 * conversionEmToPixels * 0.333333;
    else if (value == "negativeveryverythickmathspace")
        return -1 * conversionEmToPixels * 0.388889;
    else if (value == "veryverythinmathspace")
        return conversionEmToPixels * 0.055556;
    else if (value == "verythinmathspace")
        return conversionEmToPixels * 0.111111;
    else if (value == "thinmathspace")
        return conversionEmToPixels * 0.166667;
    else if (value == "mediummathspace")
        return conversionEmToPixels * 0.222222;
    else if (value == "thickmathspace")
        return conversionEmToPixels * 0.277778;
    else if (value == "verythickmathspace")
        return conversionEmToPixels * 0.333333;
    else if (value == "veryverythickmathspace")
        return conversionEmToPixels * 0.388889;
    else
        return 0;
}
