// SPDX-FileCopyrightText: 2003 Dominique Devriese <devriese@kde.org>

// SPDX-License-Identifier: GPL-2.0-or-later

#include "xfigexporter.h"

#include <math.h>

#include "../kig/kig_document.h"
#include "../kig/kig_part.h"
#include "../kig/kig_view.h"
#include "../misc/common.h"
#include "../misc/kigfiledialog.h"
#include "../misc/kigpainter.h"
#include "../objects/circle_imp.h"
#include "../objects/line_imp.h"
#include "../objects/object_drawer.h"
#include "../objects/object_holder.h"
#include "../objects/object_imp.h"
#include "../objects/other_imp.h"
#include "../objects/point_imp.h"
#include "../objects/polygon_imp.h"
#include "../objects/text_imp.h"

#include <iterator>
#include <map>

#include <QFile>
#include <QStandardPaths>
#include <QTextStream>

#include <KMessageBox>

// we need this for storing colors in a std::map..
static bool operator<(const QColor &a, const QColor &b)
{
    return a.rgb() < b.rgb();
}

XFigExporter::~XFigExporter()
{
}

QString XFigExporter::exportToStatement() const
{
    return i18n("Export to &XFig file");
}

QString XFigExporter::menuEntryName() const
{
    return i18n("&XFig File...");
}

QString XFigExporter::menuIcon() const
{
    return QStringLiteral("kig_xfig");
}

class XFigExportImpVisitor : public ObjectImpVisitor
{
    QTextStream &mstream;
    ObjectHolder *mcurobj;
    const KigWidget &mw;
    Rect msr;
    std::map<QColor, int> mcolormap;
    int mnextcolorid;
    int mcurcolorid;

    QPoint convertCoord(const Coordinate &c)
    {
        Coordinate ret = (c - msr.bottomLeft());
        ret.y = msr.height() - ret.y;
        ret *= 9450;
        ret /= msr.width();
        return ret.toQPoint();
    }

    void emitLine(const Coordinate &a, const Coordinate &b, int width, bool vector = false);

public:
    void visit(ObjectHolder *obj);
    void mapColor(const ObjectDrawer *obj);

    XFigExportImpVisitor(QTextStream &s, const KigWidget &w)
        : mstream(s)
        , mw(w)
        , msr(mw.showingRect())
        , mnextcolorid(32)
    {
        // predefined colors in XFig..
        mcolormap[Qt::black] = 0;
        mcolormap[Qt::blue] = 1;
        mcolormap[Qt::green] = 2;
        mcolormap[Qt::cyan] = 3;
        mcolormap[Qt::red] = 4;
        mcolormap[Qt::magenta] = 5;
        mcolormap[Qt::yellow] = 6;
        mcolormap[Qt::white] = 7;
    }
    using ObjectImpVisitor::visit;
    void visit(const LineImp *imp) override;
    void visit(const PointImp *imp) override;
    void visit(const TextImp *imp) override;
    void visit(const AngleImp *imp) override;
    void visit(const VectorImp *imp) override;
    void visit(const LocusImp *imp) override;
    void visit(const CircleImp *imp) override;
    void visit(const ConicImp *imp) override;
    void visit(const CubicImp *imp) override;
    void visit(const SegmentImp *imp) override;
    void visit(const RayImp *imp) override;
    void visit(const ArcImp *imp) override;
    void visit(const FilledPolygonImp *imp) override;
    void visit(const ClosedPolygonalImp *imp) override;
    void visit(const OpenPolygonalImp *imp) override;
};

void XFigExportImpVisitor::mapColor(const ObjectDrawer *obj)
{
    if (!obj->shown())
        return;
    QColor color = obj->color();
    if (mcolormap.find(color) == mcolormap.end()) {
        int newcolorid = mnextcolorid++;
        mstream << "0 " << newcolorid << " " << color.name() << "\n";
        mcolormap[color] = newcolorid;
    }
}

void XFigExportImpVisitor::visit(ObjectHolder *obj)
{
    if (!obj->drawer()->shown())
        return;
    assert(mcolormap.find(obj->drawer()->color()) != mcolormap.end());
    mcurcolorid = mcolormap[obj->drawer()->color()];
    mcurobj = obj;
    obj->imp()->visit(this);
}

void XFigExportImpVisitor::visit(const LineImp *imp)
{
    Coordinate a = imp->data().a;
    Coordinate b = imp->data().b;
    calcBorderPoints(a, b, msr);
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;

    if (a != b)
        emitLine(a, b, width);
}

void XFigExportImpVisitor::emitLine(const Coordinate &a, const Coordinate &b, int width, bool vector)
{
    mstream << "2 "; // polyline type;
    mstream << "1 "; // polyline subtype;
    mstream << "0 "; // line_style: Solid
    mstream << width << " "; // thickness: *1/80 inch
    mstream << mcurcolorid << " "; // pen_color: default
    mstream << "7 "; // fill_color: white
    mstream << "50 "; // depth: 50
    mstream << "-1 "; // pen_style: unused by XFig
    mstream << "-1 "; // area_fill: no fill
    mstream << "0.000 "; // style_val: the distance between dots and
                         // dashes in case of dotted or dashed lines..
    mstream << "0 "; // join_style: Miter
    mstream << "0 "; // cap_style: Butt
    mstream << "-1 "; // radius in case of an arc-box, but we're a
                      // polyline, so nothing here..
    if (!vector)
        mstream << "0 "; // forward arrow: no
    else
        mstream << "1 "; // forward arrow: yes
    mstream << "0 "; // backward arrow: no
    mstream << "2"; // a two points polyline..

    mstream << "\n\t ";

    if (vector) {
        // first the arrow line in case of a vector..
        mstream << "1 " // arrow_type: closed triangle
                << "1 " // arrow_style: filled with pen color..
                << "1.00 " // arrow_thickness: 1
                << "195.00 " // arrow_width
                << "165.00 " // arrow_height
                << "\n\t";
    }
    QPoint ca = convertCoord(a);
    QPoint cb = convertCoord(b);

    mstream << ca.x() << " " << ca.y() << " " << cb.x() << " " << cb.y() << "\n";
}

void XFigExportImpVisitor::visit(const PointImp *imp)
{
    const QPoint center = convertCoord(imp->coordinate());
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 5;
    width *= 10;

    mstream << "1 " // Ellipse type
            << "3 " // circle defined by radius subtype
            << "0 "; // line_style: Solid
    mstream << "1 "
            << " " // thickness: *1/80 inch
            << mcurcolorid << " " // pen_color: default
            << mcurcolorid << " " // fill_color: black
            << "50 " // depth: 50
            << "-1 " // pen_style: unused by XFig
            << "20 " // area_fill: full saturation of the fill color
            << "0.000 " // style_val: the distance between dots and
                        // dashes in case of dotted or dashed lines..
            << "1 " // direction: always 1
            << "0.0000 " // angle: the radius of the x-axis: 0
            << center.x() << " " << center.y() << " " // the center..
            << width << " " << width << " " // radius_x and radius_y
            << center.x() << " " // start_x and start_y, appear
            << center.y() << " " // unused..
            << center.x() + width << " " // end_x and end_y,
            << center.y() << "\n"; // appear unused too...
}

void XFigExportImpVisitor::visit(const TextImp *imp)
{
    QString text = imp->text();
    QPoint coord = convertCoord(imp->surroundingRect().bottomLeft());

    mstream << "4 " // text type
            << "0 " // subtype: left justfied
            << mcurcolorid << " " // color: black
            << "50 " // depth: 50
            << "-1 " // pen style: unused
            << "0 " // font: default
            << "11 " // font-size: 11
            << "0 " // angle
            << "0 " // font-flags: all the defaults..
            << "500 500 " // height, width: large enough..
            << coord.x() << " " // x, y
            << coord.y() << " " << text.toLatin1() << "\\001" // text, terminated by \001
            << "\n";
}

void XFigExportImpVisitor::visit(const AngleImp *)
{
}

void XFigExportImpVisitor::visit(const VectorImp *imp)
{
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    emitLine(imp->a(), imp->b(), width, true);
}

void XFigExportImpVisitor::visit(const LocusImp *)
{
}

void XFigExportImpVisitor::visit(const CircleImp *imp)
{
    const QPoint center = convertCoord(imp->center());
    const int radius = (convertCoord(imp->center() + Coordinate(imp->radius(), 0)) - center).x();

    mstream << "1 " // Ellipse type
            << "3 " // circle defined by radius subtype
            << "0 "; // line_style: Solid
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    mstream << width << " " // thickness: *1/80 inch
            << mcurcolorid << " " // pen_color: default
            << "7 " // fill_color: white
            << "50 " // depth: 50
            << "-1 " // pen_style: unused by XFig
            << "-1 " // area_fill: no fill
            << "0.000 " // style_val: the distance between dots and
                        // dashes in case of dotted or dashed lines..
            << "1 " // direction: always 1
            << "0.0000 " // angle: the radius of the x-axis: 0
            << center.x() << " " << center.y() << " " // the center..
            << radius << " " << radius << " " // radius_x and radius_y
            << center.x() << " " // start_x and start_y, appear
            << center.y() << " " // unused..
            << center.x() + radius << " " // end_x and end_y,
            << center.y() << "\n"; // appear unused too...
}

void XFigExportImpVisitor::visit(const ConicImp *imp)
{
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    if (imp->conicType() == 1) {
        // an ellipse, this is good, cause this allows us to export to a
        // real ellipse type..
        const ConicPolarData data = imp->polarData();

        // gather some necessary data..
        // the angle of the x axis..
        double anglex = atan2(data.esintheta0, data.ecostheta0);
        // the exentricity
        double e = hypot(data.esintheta0, data.ecostheta0);
        // the x radius is easy to find..
        double radiusx = data.pdimen / (1 - e * e);
        // the y radius is a bit harder: we first find the distance from
        // the focus point to the mid point of the two focuses, this is:
        double d = -e * data.pdimen / (1 - e * e);
        // the distance from the first focus to the intersection of the
        // second axis with the conic equals radiusx:
        double r = radiusx;
        double radiusy = sqrt(r * r - d * d);

        Coordinate center = data.focus1 - Coordinate(cos(anglex), sin(anglex)).normalize(d);
        const QPoint qcenter = convertCoord(center);
        const int radius_x = (convertCoord(center + Coordinate(radiusx, 0)) - convertCoord(center)).x();
        const int radius_y = (convertCoord(center + Coordinate(radiusy, 0)) - convertCoord(center)).x();
        const QPoint qpoint2 = convertCoord(center + Coordinate(-sin(anglex), cos(anglex)) * radiusy);

        mstream << "1 " // ellipse type
                << "1 " // subtype: ellipse defined by readii
                << "0 " // line_style: Solid
                << width << " " // thickness
                << mcurcolorid << " " // pen_color: black
                << "7 " // fill_color: white
                << "50 " // depth: 50
                << "-1 " // pan_style: not used
                << "-1 " // area_fill: no fill
                << "0.000 " // style_val: not used
                << "1 " // direction: always 1
                << anglex << " " // angle of the main axis
                << qcenter.x() << " " // center
                << qcenter.y() << " " << radius_x << " " // radiuses
                << radius_y << " " << qcenter.x() << " " // start point
                << qcenter.y() << " " << qpoint2.x() << " " // end point
                << qpoint2.y() << " ";
    } else
        return;
}

void XFigExportImpVisitor::visit(const CubicImp *)
{
}

void XFigExportImpVisitor::visit(const SegmentImp *imp)
{
    Coordinate a = imp->data().a;
    Coordinate b = imp->data().b;
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;

    emitLine(a, b, width);
}

void XFigExportImpVisitor::visit(const RayImp *imp)
{
    Coordinate a = imp->data().a;
    Coordinate b = imp->data().b;
    calcRayBorderPoints(a, b, msr);

    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;

    emitLine(a, b, width);
}

void XFigExportImpVisitor::visit(const ArcImp *imp)
{
    const Coordinate center = imp->center();
    const double radius = imp->radius();
    const double startangle = imp->startAngle();
    const double endangle = startangle + imp->angle();
    const double middleangle = (startangle + endangle) / 2;
    const Coordinate ad = Coordinate(cos(startangle), sin(startangle)).normalize(radius);
    const Coordinate bd = Coordinate(cos(middleangle), sin(middleangle)).normalize(radius);
    const Coordinate cd = Coordinate(cos(endangle), sin(endangle)).normalize(radius);
    const QPoint a = convertCoord(center + ad);
    const QPoint b = convertCoord(center + bd);
    const QPoint c = convertCoord(center + cd);
    const QPoint cent = convertCoord(center);

    mstream << "5 " // Ellipse type
            << "1 " // subtype: open ended arc
            << "0 "; // line_style: Solid
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    mstream << width << " " // thickness: *1/80 inch
            << mcurcolorid << " " // pen_color: default
            << "7 " // fill_color: white
            << "50 " // depth: 50
            << "-1 " // pen_style: unused by XFig
            << "-1 " // area_fill: no fill
            << "0.000 " // style_val: the distance between dots and
                        // dashes in case of dotted or dashed lines..
            << "0 "; // cap_style: Butt..
    // 0 is clockwise, 1 is counterclockwise .
    int direction = imp->angle() > 0 ? 1 : 0;
    // direction next
    mstream << direction << " " // direction..
            << "0 " // forward_arrow: no
            << "0 " // backward_arrow: no
            << cent.x() << " " << cent.y() << " " // the center..
            << a.x() << " " << a.y() << " " // x1, y1
            << b.x() << " " << b.y() << " " // x2, y2
            << c.x() << " " << c.y() << " " // x3, y3
            << "\n";
}

void XFigExportImpVisitor::visit(const FilledPolygonImp *imp)
{
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    const std::vector<Coordinate> oldpts = imp->points();
    // it has n points, but the first point is counted twice (ie. as ending too)
    std::vector<Coordinate> pts;
    std::copy(oldpts.begin(), oldpts.end(), std::back_inserter(pts));
    pts.push_back(pts[0]);
    mstream << "2 "; // polyline type;
    mstream << "3 "; // polygon subtype;
    mstream << "0 "; // line_style: Solid
    mstream << width << " "; // thickness: *1/80 inch
    mstream << mcurcolorid << " "; // pen_color: our color
    mstream << mcurcolorid << " "; // fill_color: our color
    mstream << "50 "; // depth: 50
    mstream << "-1 "; // pen_style: unused by XFig
    mstream << "10 "; // area_fill: 50% of opacity
    mstream << "0.000 "; // style_val: the distance between dots and
                         // dashes in case of dotted or dashed lines..
    mstream << "0 "; // join_style: Miter
    mstream << "0 "; // cap_style: Butt
    mstream << "-1 "; // radius in case of an arc-box, but we're a
                      // polygon, so nothing here..
    mstream << "0 "; // forward arrow: no
    mstream << "0 "; // backward arrow: no
    mstream << pts.size(); // it has n (well, n+1) points
    mstream << "\n";

    // write the list of points, max 6 per line..
    bool in_line = false;
    for (uint i = 0; i < pts.size(); ++i) {
        int m = i % 6;
        if (m == 0) {
            in_line = true;
            mstream << "\t";
        }
        QPoint p = convertCoord(pts[i]);
        mstream << " " << p.x() << " " << p.y();
        if (m == 5) {
            in_line = false;
            mstream << "\n";
        }
    }
    if (in_line)
        mstream << "\n";
}

void XFigExportImpVisitor::visit(const ClosedPolygonalImp *imp)
{
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    const std::vector<Coordinate> oldpts = imp->points();
    // it has n points, but the first point is counted twice (ie. as ending too)
    std::vector<Coordinate> pts;
    std::copy(oldpts.begin(), oldpts.end(), std::back_inserter(pts));
    pts.push_back(pts[0]);
    mstream << "2 "; // polyline type;
    mstream << "3 "; // polygon subtype;
    mstream << "0 "; // line_style: Solid
    mstream << width << " "; // thickness: *1/80 inch
    mstream << mcurcolorid << " "; // pen_color: our color
    mstream << mcurcolorid << " "; // fill_color: our color
    mstream << "50 "; // depth: 50
    mstream << "-1 "; // pen_style: unused by XFig
    mstream << "20 "; // area_fill: 0% of opacity
    mstream << "0.000 "; // style_val: the distance between dots and
                         // dashes in case of dotted or dashed lines..
    mstream << "0 "; // join_style: Miter
    mstream << "0 "; // cap_style: Butt
    mstream << "-1 "; // radius in case of an arc-box, but we're a
                      // polygon, so nothing here..
    mstream << "0 "; // forward arrow: no
    mstream << "0 "; // backward arrow: no
    mstream << pts.size(); // it has n (well, n+1) points
    mstream << "\n";

    // write the list of points, max 6 per line..
    bool in_line = false;
    for (uint i = 0; i < pts.size(); ++i) {
        int m = i % 6;
        if (m == 0) {
            in_line = true;
            mstream << "\t";
        }
        QPoint p = convertCoord(pts[i]);
        mstream << " " << p.x() << " " << p.y();
        if (m == 5) {
            in_line = false;
            mstream << "\n";
        }
    }
    if (in_line)
        mstream << "\n";
}
void XFigExportImpVisitor::visit(const OpenPolygonalImp *imp)
{
    int width = mcurobj->drawer()->width();
    if (width == -1)
        width = 1;
    const std::vector<Coordinate> pts = imp->points();
    mstream << "2 "; // polyline type;
    mstream << "3 "; // polygon subtype;
    mstream << "0 "; // line_style: Solid
    mstream << width << " "; // thickness: *1/80 inch
    mstream << mcurcolorid << " "; // pen_color: our color
    mstream << mcurcolorid << " "; // fill_color: our color
    mstream << "50 "; // depth: 50
    mstream << "-1 "; // pen_style: unused by XFig
    mstream << "20 "; // area_fill: 0% of opacity
    mstream << "0.000 "; // style_val: the distance between dots and
                         // dashes in case of dotted or dashed lines..
    mstream << "0 "; // join_style: Miter
    mstream << "0 "; // cap_style: Butt
    mstream << "-1 "; // radius in case of an arc-box, but we're a
                      // polygon, so nothing here..
    mstream << "0 "; // forward arrow: no
    mstream << "0 "; // backward arrow: no
    mstream << pts.size(); // it has n (well, n+1) points
    mstream << "\n";

    // write the list of points, max 6 per line..
    bool in_line = false;
    for (uint i = 0; i < pts.size(); ++i) {
        int m = i % 6;
        if (m == 0) {
            in_line = true;
            mstream << "\t";
        }
        QPoint p = convertCoord(pts[i]);
        mstream << " " << p.x() << " " << p.y();
        if (m == 5) {
            in_line = false;
            mstream << "\n";
        }
    }
    if (in_line)
        mstream << "\n";
}

void XFigExporter::run(const KigPart &doc, KigWidget &w)
{
    KigFileDialog *kfd =
        new KigFileDialog(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation), i18n("XFig Documents (*.fig)"), i18n("Export as XFig File"), &w);
    if (!kfd->exec())
        return;

    QString file_name = kfd->selectedFile();

    delete kfd;

    QFile file(file_name);
    if (!file.open(QIODevice::WriteOnly)) {
        KMessageBox::error(&w,
                           i18n("The file \"%1\" could not be opened. Please "
                                "check if the file permissions are set correctly.",
                                file_name));
        return;
    };
    QTextStream stream(&file);
    stream << "#FIG 3.2  Produced by Kig\n";
    stream << "Landscape\n";
    stream << "Center\n";
    stream << "Metric\n";
    stream << "A4\n";
    stream << "100.00\n";
    stream << "Single\n";
    stream << "-2\n";
    stream << "1200 2\n";

    std::vector<ObjectHolder *> os = doc.document().objects();
    XFigExportImpVisitor visitor(stream, w);

    for (std::vector<ObjectHolder *>::const_iterator i = os.begin(); i != os.end(); ++i) {
        visitor.mapColor((*i)->drawer());
    };

    for (std::vector<ObjectHolder *>::const_iterator i = os.begin(); i != os.end(); ++i) {
        visitor.visit(*i);
    };
}
