/************************************************************************
 **
 **  @file   vplayoutfilewriter.cpp
 **  @author Ronan Le Tiec
 **  @date   18 4, 2020
 **
 **  @brief
 **  @copyright
 **  This source code is part of the Valentina project, a pattern making
 **  program, whose allow create and modeling patterns of clothing.
 **  Copyright (C) 2020 Valentina project
 **  <https://gitlab.com/smart-pattern/valentina> All Rights Reserved.
 **
 **  Valentina is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  Valentina 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 General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with Valentina.  If not, see <http://www.gnu.org/licenses/>.
 **
 *************************************************************************/

#include "vplayoutfilewriter.h"
#include "../ifc/xml/vlayoutconverter.h"
#include "../layout/vplayout.h"
#include "../layout/vppiece.h"
#include "../layout/vpsheet.h"
#include "../vgeometry/vlayoutplacelabel.h"
#include "../vlayout/vlayoutpiecepath.h"
#include "../vlayout/vtextmanager.h"
#include "../vmisc/projectversion.h"
#include "def.h"
#include "qnumeric.h"
#include "vplayoutliterals.h"

namespace
{
//---------------------------------------------------------------------------------------------------------------------
template <class T> auto NumberToString(T number) -> QString
{
    const QLocale locale = QLocale::c();
    return locale.toString(number, 'g', 12).remove(LocaleGroupSeparator(locale));
}

//---------------------------------------------------------------------------------------------------------------------
auto TransformToString(const QTransform &m) -> QString
{
    QStringList const matrix{NumberToString(m.m11()), NumberToString(m.m12()), NumberToString(m.m13()),
                             NumberToString(m.m21()), NumberToString(m.m22()), NumberToString(m.m23()),
                             NumberToString(m.m31()), NumberToString(m.m32()), NumberToString(m.m33())};
    return matrix.join(ML::groupSep);
}

//---------------------------------------------------------------------------------------------------------------------
auto PointToString(const QPointF &p) -> QString
{
    return NumberToString(p.x()) + ML::coordintatesSep + NumberToString(p.y());
}

//---------------------------------------------------------------------------------------------------------------------
auto PathToString(const QVector<QPointF> &pathPoints) -> QString
{
    QStringList path;
    path.reserve(pathPoints.size());

    for (auto point : pathPoints)
    {
        path.append(PointToString(point));
    }

    return path.join(ML::pointsSep);
}

//---------------------------------------------------------------------------------------------------------------------
auto RectToString(const QRectF &r) -> QString
{
    return NumberToString(r.x()) + ML::groupSep + NumberToString(r.y()) + ML::groupSep + NumberToString(r.width()) +
           ML::groupSep + NumberToString(r.height());
}

//---------------------------------------------------------------------------------------------------------------------
auto LineToString(const QLineF &line) -> QString
{
    return PointToString(line.p1()) + ML::groupSep + PointToString(line.p2());
}

//---------------------------------------------------------------------------------------------------------------------
auto LinesToString(const QVector<QLineF> &lines) -> QString
{
    QStringList l;
    l.reserve(lines.size());
    for (const auto &line : lines)
    {
        l.append(LineToString(line));
    }
    return l.join(ML::itemsSep);
}

//---------------------------------------------------------------------------------------------------------------------
auto GrainlineArrowDirrectionToString(GrainlineArrowDirection type) -> QString
{
    switch (type)
    {
        case GrainlineArrowDirection::oneWayUp:
            return ML::oneWayUpStr;
        case GrainlineArrowDirection::oneWayDown:
            return ML::oneWayDownStr;
        case GrainlineArrowDirection::fourWays:
            return ML::fourWaysStr;
        case GrainlineArrowDirection::twoWaysUpLeft:
            return ML::twoWaysUpLeftStr;
        case GrainlineArrowDirection::twoWaysUpRight:
            return ML::twoWaysUpRightStr;
        case GrainlineArrowDirection::twoWaysDownLeft:
            return ML::twoWaysDownLeftStr;
        case GrainlineArrowDirection::twoWaysDownRight:
            return ML::twoWaysDownRightStr;
        case GrainlineArrowDirection::threeWaysUpDownLeft:
            return ML::threeWaysUpDownLeftStr;
        case GrainlineArrowDirection::threeWaysUpDownRight:
            return ML::threeWaysUpDownRightStr;
        case GrainlineArrowDirection::threeWaysUpLeftRight:
            return ML::threeWaysUpLeftRightStr;
        case GrainlineArrowDirection::threeWaysDownLeftRight:
            return ML::threeWaysDownLeftRightStr;
        case GrainlineArrowDirection::twoWaysUpDown:
        default:
            return ML::twoWaysUpDownStr;
    }
}
} // namespace

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteFile(const VPLayoutPtr &layout, QIODevice *file)
{
    setDevice(file);
    setAutoFormatting(true);

    writeStartDocument();
    writeComment(
        QStringLiteral("Layout created with Valentina v%1 (https://smart-pattern.com.ua/).").arg(AppVersionStr()));
    WriteLayout(layout);
    writeEndDocument();
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteLayout(const VPLayoutPtr &layout)
{
    writeStartElement(ML::TagLayout);
    SetAttribute(AttrLayoutVersion, VLayoutConverter::LayoutMaxVerStr);
    WriteLayoutProperties(layout);
    WritePieceList(layout->GetUnplacedPieces(), ML::TagUnplacedPieces);
    WriteSheets(layout);
    writeEndElement(); // layout
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteLayoutProperties(const VPLayoutPtr &layout)
{
    writeStartElement(ML::TagProperties);

    writeTextElement(ML::TagUnit, UnitsToStr(layout->LayoutSettings().GetUnit()));
    writeTextElement(ML::TagTitle, layout->LayoutSettings().GetTitle());
    writeTextElement(ML::TagDescription, layout->LayoutSettings().GetDescription());

    writeStartElement(ML::TagControl);
    SetAttribute(ML::AttrWarningSuperposition, layout->LayoutSettings().GetWarningSuperpositionOfPieces());
    SetAttribute(ML::AttrWarningOutOfBound, layout->LayoutSettings().GetWarningPiecesOutOfBound());
    SetAttribute(ML::AttrWarningPieceGape, layout->LayoutSettings().GetWarningPieceGapePosition());
    SetAttribute(ML::AttrStickyEdges, layout->LayoutSettings().IsStickyEdges());
    SetAttribute(ML::AttrPiecesGap, layout->LayoutSettings().GetPiecesGap());
    SetAttribute(ML::AttrFollowGrainline, layout->LayoutSettings().GetFollowGrainline());
    SetAttribute(ML::AttrBoundaryTogetherWithNotches, layout->LayoutSettings().IsBoundaryTogetherWithNotches());
    SetAttributeOrRemoveIf<bool>(ML::AttrCutOnFold, layout->LayoutSettings().IsCutOnFold(),
                                 [](bool cut) noexcept { return not cut; });
    writeEndElement(); // control

    WriteTiles(layout);

    writeStartElement(ML::TagScale);
    SetAttribute(ML::AttrXScale, layout->LayoutSettings().HorizontalScale());
    SetAttribute(ML::AttrYScale, layout->LayoutSettings().VerticalScale());
    writeEndElement(); // scale

    writeStartElement(ML::TagWatermark);
    SetAttributeOrRemoveIf<bool>(ML::AttrShowPreview, layout->LayoutSettings().GetShowWatermark(),
                                 [](bool show) noexcept { return not show; });
    writeCharacters(layout->LayoutSettings().WatermarkPath());
    writeEndElement(); // watermark

    writeEndElement(); // properties
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteSheets(const VPLayoutPtr &layout)
{
    writeStartElement(ML::TagSheets);

    QList<VPSheetPtr> const sheets = layout->GetSheets();
    for (const auto &sheet : sheets)
    {
        if (not sheet.isNull())
        {
            WriteSheet(sheet);
        }
    }

    writeEndElement(); // sheets
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteSheet(const VPSheetPtr &sheet)
{
    writeStartElement(ML::TagSheet);
    SetAttributeOrRemoveIf<QString>(ML::AttrGrainlineType, GrainlineTypeToStr(sheet->GetGrainlineType()),
                                    [](const QString &type) noexcept
                                    { return type == GrainlineTypeToStr(GrainlineType::NotFixed); });

    writeTextElement(ML::TagName, sheet->GetName());
    WriteSize(sheet->GetSheetSize());
    WriteMargins(sheet->GetSheetMargins(), sheet->IgnoreMargins());
    WritePieceList(sheet->GetPieces(), ML::TagPieces);

    writeEndElement(); // sheet
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteTiles(const VPLayoutPtr &layout)
{
    writeStartElement(ML::TagTiles);
    SetAttribute(ML::AttrVisible, layout->LayoutSettings().GetShowTiles());
    SetAttribute(ML::AttrMatchingMarks, "standard"); // TODO / Fixme get the right value
    SetAttributeOrRemoveIf<bool>(ML::AttrPrintScheme, layout->LayoutSettings().GetPrintTilesScheme(),
                                 [](bool print) noexcept { return not print; });
    SetAttributeOrRemoveIf<bool>(ML::AttrTileNumber, layout->LayoutSettings().GetShowTileNumber(),
                                 [](bool show) noexcept { return not show; });

    WriteSize(layout->LayoutSettings().GetTilesSize());
    WriteMargins(layout->LayoutSettings().GetTilesMargins(), layout->LayoutSettings().IgnoreTilesMargins());

    writeEndElement(); // tiles
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WritePieceList(const QList<VPPiecePtr> &list, const QString &tagName)
{
    writeStartElement(tagName); // piece list
    for (const auto &piece : list)
    {
        WritePiece(piece);
    }

    writeEndElement(); // piece list
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WritePiece(const VPPiecePtr &piece)
{
    writeStartElement(ML::TagPiece);
    SetAttribute(ML::AttrUID, piece->GetUUID().toString());
    SetAttribute(ML::AttrName, piece->GetName());
    SetAttributeOrRemoveIf<bool>(ML::AttrVerticallyFlipped, piece->IsVerticallyFlipped(),
                                 [](bool flipped) noexcept { return not flipped; });
    SetAttributeOrRemoveIf<bool>(ML::AttrHorizontallyFlipped, piece->IsHorizontallyFlipped(),
                                 [](bool flipped) noexcept { return not flipped; });
    SetAttributeOrRemoveIf<bool>(ML::AttrForbidFlipping, piece->IsForbidFlipping(),
                                 [](bool forbid) noexcept { return not forbid; });
    SetAttributeOrRemoveIf<bool>(ML::AttrForceFlipping, piece->IsForceFlipping(),
                                 [](bool force) noexcept { return not force; });
    SetAttributeOrRemoveIf<bool>(ML::AttrFollowGrainline, piece->IsFollowGrainline(),
                                 [](bool follow) noexcept { return not follow; });
    SetAttributeOrRemoveIf<bool>(ML::AttrSewLineOnDrawing, piece->IsSewLineOnDrawing(),
                                 [](bool value) noexcept { return not value; });
    SetAttribute(ML::AttrTransform, TransformToString(piece->GetMatrix()));
    SetAttributeOrRemoveIf<QString>(ML::AttrGradationLabel, piece->GetGradationId(),
                                    [](const QString &label) noexcept { return label.isEmpty(); });
    SetAttribute(ML::AttrCopyNumber, piece->CopyNumber());
    SetAttributeOrRemoveIf<bool>(ML::AttrShowSeamline, not piece->IsHideMainPath(),
                                 [](bool show) noexcept { return show; });
    SetAttributeOrRemoveIf<qreal>(ML::AttrXScale, piece->GetXScale(),
                                  [](qreal xs) noexcept { return VFuzzyComparePossibleNulls(xs, 1.0); });
    SetAttributeOrRemoveIf<qreal>(ML::AttrYScale, piece->GetYScale(),
                                  [](qreal ys) noexcept { return VFuzzyComparePossibleNulls(ys, 1.0); });
    SetAttributeOrRemoveIf<qreal>(ML::AttrZValue, piece->ZValue(),
                                  [](qreal z) noexcept { return VFuzzyComparePossibleNulls(z, 1.0); });
    SetAttributeOrRemoveIf<bool>(ML::AttrShowFullPiece, piece->IsShowFullPiece(),
                                 [](bool show) noexcept { return show; });

    writeStartElement(ML::TagSeamLine);
    QVector<VLayoutPoint> const contourPoints = piece->GetContourPoints();
    for (const auto &point : contourPoints)
    {
        WriteLayoutPoint(point);
    }
    writeEndElement();

    writeStartElement(ML::TagSeamAllowance);
    SetAttributeOrRemoveIf<bool>(ML::AttrEnabled, piece->IsSeamAllowance(),
                                 [](bool enabled) noexcept { return not enabled; });
    SetAttributeOrRemoveIf<bool>(ML::AttrBuiltIn, piece->IsSeamAllowanceBuiltIn(),
                                 [](bool builtin) noexcept { return not builtin; });
    if (piece->IsSeamAllowance() && not piece->IsSeamAllowanceBuiltIn())
    {
        QVector<VLayoutPoint> const seamAllowancePoints = piece->GetSeamAllowancePoints();
        for (const auto &point : seamAllowancePoints)
        {
            WriteLayoutPoint(point);
        }
    }
    writeEndElement();

    writeStartElement(ML::TagGrainline);
    SetAttributeOrRemoveIf<bool>(ML::AttrEnabled, piece->IsGrainlineEnabled(),
                                 [](bool enabled) noexcept { return not enabled; });
    SetAttributeOrRemoveIf<bool>(ML::AttrVisible, piece->IsGrainlineVisible(),
                                 [](bool visible) noexcept { return visible; });
    if (piece->IsGrainlineEnabled())
    {
        SetAttribute(ML::AttrArrowDirection, GrainlineArrowDirrectionToString(piece->GetGrainline().GetArrowType()));
        writeCharacters(LineToString(piece->GetGrainlineMainLine()));
    }
    writeEndElement();

    writeStartElement(ML::TagNotches);
    QVector<VLayoutPassmark> const passmarks = piece->GetPassmarks();
    for (const auto &passmark : passmarks)
    {
        writeStartElement(ML::TagNotch);
        SetAttribute(ML::AttrBuiltIn, passmark.isBuiltIn);
        SetAttribute(ML::AttrType, static_cast<int>(passmark.type));
        SetAttribute(ML::AttrBaseLine, LineToString(passmark.baseLine));
        SetAttribute(ML::AttrPath, LinesToString(passmark.lines));
        SetAttributeOrRemoveIf<bool>(ML::AttrClockwiseOpening, passmark.isClockwiseOpening,
                                     [](bool clockwise) noexcept { return not clockwise; });
        writeEndElement();
    }
    writeEndElement();

    writeStartElement(ML::TagInternalPaths);
    QVector<VLayoutPiecePath> const internalPaths = piece->GetInternalPaths();
    for (const auto &path : internalPaths)
    {
        writeStartElement(ML::TagInternalPath);
        SetAttribute(ML::AttrCut, path.IsCutPath());
        SetAttribute(ML::AttrPenStyle, PenStyleToLineStyle(path.PenStyle()));
        SetAttributeOrRemoveIf<bool>(ML::AttrNotMirrored, path.IsNotMirrored(),
                                     [](bool mirrored) noexcept { return mirrored; });

        QVector<VLayoutPoint> const points = path.Points();
        for (const auto &point : points)
        {
            WriteLayoutPoint(point);
        }

        writeEndElement();
    }
    writeEndElement();

    writeStartElement(ML::TagMarkers);
    QVector<VLayoutPlaceLabel> const placelabels = piece->GetPlaceLabels();
    for (const auto &label : placelabels)
    {
        writeStartElement(ML::TagMarker);
        SetAttribute(ML::AttrTransform, TransformToString(label.RotationMatrix()));
        SetAttribute(ML::AttrType, static_cast<int>(label.Type()));
        SetAttribute(ML::AttrCenter, PointToString(label.Center()));
        SetAttribute(ML::AttrBox, RectToString(label.Box()));
        SetAttributeOrRemoveIf<bool>(ML::AttrNotMirrored, label.IsNotMirrored(),
                                     [](bool mirrored) noexcept { return mirrored; });
        writeEndElement();
    }
    writeEndElement();

    writeStartElement(ML::TagLabels);
    WriteLabel(piece->GetPieceLabelRect(), piece->GetPieceLabelData(), ML::TagPieceLabel);
    WriteLabel(piece->GetPatternLabelRect(), piece->GetPatternLabelData(), ML::TagPatternLabel);
    writeEndElement();

    QLineF const seamMirrorLine = piece->GetSeamMirrorLine();
    if (QLineF const seamAllowenceMirrorLine = piece->GetSeamAllowanceMirrorLine();
        !seamMirrorLine.isNull() || !seamAllowenceMirrorLine.isNull())
    {
        writeStartElement(ML::TagMirrorLine);

        SetAttribute(ML::AttrFoldLineType, FoldLineTypeToString(piece->GetFoldLineType()));
        SetAttributeOrRemoveIf<qreal>(ML::AttrFoldLineHeight, piece->GetFoldLineHeight(),
                                      [](qreal height) noexcept { return qFuzzyIsNull(height); });
        SetAttributeOrRemoveIf<qreal>(ML::AttrFoldLineWidth, piece->GetFoldLineWidth(),
                                      [](qreal width) noexcept { return qFuzzyIsNull(width); });
        SetAttributeOrRemoveIf<qreal>(ML::AttrFoldLineCenter, piece->GetFoldLineCenterPosition(),
                                      [](qreal center) noexcept { return VFuzzyComparePossibleNulls(center, 0.5); });
        SetAttributeOrRemoveIf<bool>(ML::AttrItalic, piece->IsFoldLineLabelFontItalic(),
                                     [](bool italic) noexcept { return not italic; });
        SetAttributeOrRemoveIf<bool>(ML::AttrBold, piece->IsFoldLineLabelFontBold(),
                                     [](bool bold) noexcept { return not bold; });
        SetAttributeOrRemoveIf<QString>(ML::AttrFoldLineLabel, piece->GetFoldLineLabel(),
                                        [](const QString &label) noexcept { return label.isEmpty(); });
        SetAttributeOrRemoveIf<int>(ML::AttrAlignment, piece->GetFoldLineLabelAlignment(),
                                    [](int alignment) noexcept { return alignment == Qt::AlignHCenter; });
        SetAttributeOrRemoveIf<QString>(ML::AttrFont, piece->GetFoldLineOutlineFont().toString(),
                                        [piece](const QString &) { return piece->GetFoldLineLabel().isEmpty(); });
        SetAttributeOrRemoveIf<QString>(
            ML::AttrSVGFont,
            QStringLiteral("%1,%2").arg(piece->GetFoldLineSVGFontFamily()).arg(piece->GetFoldLineSvgFontSize()),
            [piece](const QString &) { return piece->GetFoldLineLabel().isEmpty(); });
        SetAttributeOrRemoveIf<bool>(ML::AttrVisible, piece->IsShowMirrorLine(),
                                     [](bool visible) noexcept { return visible; });

        if (!seamMirrorLine.isNull())
        {
            writeStartElement(ML::TagSeamLine);
            writeCharacters(LineToString(seamMirrorLine));
            writeEndElement();
        }

        if (!seamAllowenceMirrorLine.isNull())
        {
            writeStartElement(ML::TagSeamAllowance);
            writeCharacters(LineToString(seamAllowenceMirrorLine));
            writeEndElement();
        }

        writeEndElement();
    }

    writeEndElement();
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteLabel(const QVector<QPointF> &labelShape, const VTextManager &tm, const QString &tagName)
{
    if (labelShape.size() > 2 && tm.GetSourceLinesCount() > 0)
    {
        writeStartElement(tagName);
        SetAttribute(ML::AttrShape, PathToString(labelShape));
        WriteLabelLines(tm);
        writeEndElement();
    }
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteLabelLines(const VTextManager &tm)
{
    writeStartElement(ML::TagLines);
    SetAttribute(ML::AttrFont, tm.GetFont().toString());
    SetAttribute(ML::AttrSVGFont, QStringLiteral("%1,%2").arg(tm.GetSVGFontFamily()).arg(tm.GetSVGFontPointSize()));

    for (int i = 0; i < tm.GetSourceLinesCount(); ++i)
    {
        writeStartElement(ML::TagLine);
        const TextLine &tl = tm.GetSourceLine(i);
        SetAttribute(ML::AttrFontSize, tl.m_iFontSize);
        SetAttribute(ML::AttrBold, tl.m_bold);
        SetAttribute(ML::AttrItalic, tl.m_italic);
        SetAttribute(ML::AttrAlignment, static_cast<int>(tl.m_eAlign));
        writeCharacters(tl.m_qsText);
        writeEndElement();
    }
    writeEndElement();
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteMargins(const QMarginsF &margins, bool ignore)
{
    writeStartElement(ML::TagMargin);

    SetAttributeOrRemoveIf<qreal>(ML::AttrLeft, margins.left(), [](qreal margin) noexcept { return margin <= 0; });
    SetAttributeOrRemoveIf<qreal>(ML::AttrTop, margins.top(), [](qreal margin) noexcept { return margin <= 0; });
    SetAttributeOrRemoveIf<qreal>(ML::AttrRight, margins.right(), [](qreal margin) noexcept { return margin <= 0; });
    SetAttributeOrRemoveIf<qreal>(ML::AttrBottom, margins.bottom(), [](qreal margin) noexcept { return margin <= 0; });

    SetAttributeOrRemoveIf<bool>(ML::AttrIgnoreMargins, ignore, [](bool ignore) noexcept { return not ignore; });

    writeEndElement(); // margin
}

//---------------------------------------------------------------------------------------------------------------------
void VPLayoutFileWriter::WriteSize(QSizeF size)
{
    // maybe not necessary to test this, the writer should "stupidly write", the application should take care of these
    // tests
    qreal width = size.width();
    if (width < 0)
    {
        width = 0;
    }

    qreal length = size.height();
    if (length < 0)
    {
        length = 0;
    }

    writeStartElement(ML::TagSize);
    SetAttribute(ML::AttrWidth, width);
    SetAttribute(ML::AttrLength, length);
    writeEndElement(); // size
}

//---------------------------------------------------------------------------------------------------------------------
auto VPLayoutFileWriter::WriteLayoutPoint(const VLayoutPoint &point) -> void
{
    writeStartElement(ML::TagPoint);
    SetAttribute(ML::AttrX, point.x());
    SetAttribute(ML::AttrY, point.y());
    SetAttributeOrRemoveIf<bool>(ML::AttrTurnPoint, point.TurnPoint(), [](bool val) noexcept { return val; });
    SetAttributeOrRemoveIf<bool>(ML::AttrCurvePoint, point.CurvePoint(), [](bool val) noexcept { return val; });
    writeEndElement();
}
