#include "ipefiledata.h"
#include "drawingarea.h"
#include "probleminstance.h"
#include "routenetwork.h"
#include "labelmodel.h"
#include "assertions.h"
#include <cstring>
#include <unordered_map>
#include <unordered_set>
#include <QByteArray>
#include <QBuffer>
#include <QtEndian>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>

#include <iostream> // TODO: remove

namespace {

////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
////////////////////////////////////////////////////////////////////////

int parseUint(unsigned int& output, QString const& line, int start = 0)
{
    while (start < line.length() && line[start] == ' ') {
        ++start;
    }
    int i = line.indexOf(' ', start);
    if (i < 0) i = line.length();
    bool parsedCorrectly = false;
    output = line.mid(start, i-start).toUInt(&parsedCorrectly);
    if (parsedCorrectly) {
        return i;
    } else {
        return -1;
    }
}

int parseReal(qreal& output, QString const& line, int start = 0)
{
    while (start < line.length() && line[start] == ' ') {
        ++start;
    }
    int i = line.indexOf(' ', start);
    if (i < 0) i = line.length();
    bool parsedCorrectly = false;
    output = line.mid(start, i-start).toDouble(&parsedCorrectly);
    if (parsedCorrectly) {
        return i;
    } else {
        return -1;
    }
}

int parsePoint(QPointF& output, QString const& line, int start = 0)
{
    start = parseReal(output.rx(), line, start);
    if (start < 0) {
        return -1;
    }
    start = parseReal(output.ry(), line, start);
    if (start < 0) {
        return -1;
    }
    return start;
}

int parsePoints(QPointF* output, int n, QString const& line, int start = 0)
{
    for (int i = 0; i < n; ++i) {
        start = parsePoint(output[i], line, start);
        if (start < 0) {
            return false;
        }
    }
    return start;
}

int parseRect(QPointF* output, QString const& line, int start = 0)
{
    return parsePoints(output, 2, line, start);
}
int parseMatrix(QPointF* output, QString const& line, int start = 0)
{
    return parsePoints(output, 3, line, start);
}

QPointF applyMatrix(QPointF* matrix, QPointF const& point)
{
    return QPointF( matrix[0].x()*point.x() + matrix[1].x()*point.y() + matrix[2].x(),
                    matrix[0].y()*point.x() + matrix[1].y()*point.y() + matrix[2].y() );
}

QString unparseUint(unsigned int x)
{
    QString result;
    result.setNum(x);
    return result;
}

QString unparseReal(qreal x)
{
    QString result;
    result.setNum(x, 'g', 17);
    assert(x == result.toDouble());
    return result;
}

QString escapeString(QString str) {
    return str.replace('\\', "\\\\")
              .replace('"',  "\\\"")
              .replace('\n', "\\n")
              .replace('\r', "\\r")
              .replace('\t', "\\t")
              .replace('\v', "\\v")
              .replace('\b', "\\b")
              .replace('\f', "\\f");
}
QString unescapeString(QString str) {
    QString result;
    result.reserve(str.size());
    for (int i = 0; i < str.size(); ++i) {
        QChar c1 = str[i];
        if (c1 != '\\' || i+1 >= str.size()) {
            result.append(c1);
        } else {
            QChar c2 = str[++i];
            switch (c2.toAscii()) {
            case '\\': case '"': result.append(c2); break;
            case 'n': result.append('\n'); break;
            case 'r': result.append('\r'); break;
            case 't': result.append('\t'); break;
            case 'v': result.append('\v'); break;
            case 'b': result.append('\b'); break;
            case 'f': result.append('\f'); break;
            default: result.append(c2);
            }
        }
    }
    return result;
}

struct RouteParameters
{
    QString labelTemplate;
    qreal airplaneSpeed;
    qreal interarrivalTime;
    unsigned int seed;
    bool usesPoisson;

    void applyToRoute(AirplaneRoute& route) {
        route.labelTemplate(labelTemplate);
        route.airplaneSpeed(airplaneSpeed);
        route.averageInterarrivalTime(interarrivalTime);
        route.rngSeed(seed);
        route.usesPoissonProcess(usesPoisson);
    }
};

int parseRouteParameters(RouteParameters& params, QString const& line, int start = 0)
{
    start = parseUint(params.seed, line, start);
    if (start < 0) {
        return start;
    }
    start = parseReal(params.airplaneSpeed, line, start);
    if (start < 0) {
        return start;
    }
    start = parseReal(params.interarrivalTime, line, start);
    if (start < 0) {
        return start;
    }
    unsigned int i = 0;
    start = parseUint(i, line, start);
    if (start < 0) {
        return start;
    }
    params.usesPoisson = i > 0;
    params.labelTemplate = unescapeString( line.mid(start+1) );
    return line.size();
}
QString unparseRouteParameters(AirplaneRoute const& route)
{
    QString result;
    result.reserve(20+route.labelTemplate().size());
    result.append( unparseUint(route.rngSeed()) );
    result.append(" ");
    result.append( unparseReal(route.airplaneSpeed()) );
    result.append(" ");
    result.append( unparseReal(route.averageInterarrivalTime()) );
    result.append(" ");
    result.append( unparseUint(route.usesPoissonProcess() ? 1 : 0) );
    result.append(" ");
    result.append( escapeString(route.labelTemplate()) );
    return result;
}

// Ipe's coordinate system has positive Y in the upward direction,
// Qt's has it in the downward direction.
enum { IPE_OFFSET = 842 }; // seems about right to align the top of the Ipe canvas with that of Qt
QPointF ipeFlip(QPointF const& point) { return QPointF(point.x(), IPE_OFFSET - point.y()); }

QString unparsePoint(QPointF const& point)
{
    QPointF flipped = ipeFlip(point);
    return unparseReal(flipped.x()) + ' ' + unparseReal(flipped.y());
}




////////////////////////////////////////////////////////////////////////
// IPE FILE READING
////////////////////////////////////////////////////////////////////////

class IpeReader {
public:
    IpeReader(QString filePath_, ProblemInstance& problemInstance_,
              QByteArray* ipeSection_, std::vector< std::pair<QRectF, QImage> >& backgroundImages_)
        : filePath(filePath_), problemInstance(problemInstance_), ipeSection(ipeSection_),
          backgroundImages(backgroundImages_), lastWriteWasNewline(false), readLabelModel(false)
    {
        QFile file(filePath.isEmpty() ? ":/empty.ipe" : filePath);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
            raiseError(filePath.isEmpty()
                       ? QString("couldn't open new file template")
                       : QString("couldn't open file '%1' for reading").arg(filePath), 0);

        reader.setDevice(&file);

        switchToIpeSection(0);
        readBitmapsAndStylesheets();

        switchToIpeSection(1);
        readFirstPage();

        switchToIpeSection(2);
        readOtherPages();

        if (!readLabelModel) {
            problemInstance.setLabelModel(LabelModel::defaultModel());
        }
    }

private:
    // input
    QString filePath;

    // output
    ProblemInstance& problemInstance;
    QByteArray* ipeSection;
    std::vector< std::pair<QRectF, QImage> >& backgroundImages;

    // temporaries
    QXmlStreamReader reader;
    QXmlStreamWriter writer;
    QBuffer buffer;
    std::vector<QImage> images;
    std::unordered_map<std::string, RouteParameters> routeParameters;
    bool lastWriteWasNewline;
    bool readLabelModel;

private:
    void readBitmapsAndStylesheets();
    void readBitmap();
    void readInstanceAndRouteAttributes();

    void readFirstPage();
    void readRoute()        { readRouteOrNetworkEdges(true); }
    void readNetworkEdges() { readRouteOrNetworkEdges(false); }
    void readRouteOrNetworkEdges(bool isRoute);
    void readImageReference(QString const& layer);

    void readOtherPages();

    void switchToIpeSection(int i);
    QString attr(char const* qualifiedName);
    bool atNewline();

    template <typename String>
    void raiseError(String&& msg, qint64 lineNumber = -1) {
        throw InstanceReadError(filePath, lineNumber >= 0 ? lineNumber : reader.lineNumber(), std::forward<String>(msg));
    }
    void raiseError(QString&& msg, qint64 lineNumber = -1) {
        raiseError(msg.toUtf8().data(), lineNumber);
    }

    void transferToken();
    void transferElement() { transferElementWithLayer(QString()); }
    void transferElementWithLayer(QString layerName);
};

void IpeReader::switchToIpeSection(int i) {
    buffer.close();
    buffer.setBuffer(&ipeSection[i]);
    buffer.open(QIODevice::WriteOnly);
    writer.setDevice(&buffer);
}
QString IpeReader::attr(char const* qualifiedName)
{
    return reader.attributes().value(qualifiedName).toString();
}
bool isNewline(QChar c) {
    return c == '\n'; // NOTE: doesn't catch all Unicode line terminators, but works for our application
}                     // Qt doesn't seem to provide a convenient QChar method to do this the "right" way.
bool isWhitespaceIncludingNewline(QStringRef s)
{
    bool hasNewline = false;
    QChar const* end = s.data() + s.length();
    for (QChar const* p = s.data(); p != end; ++p) {
        if (!p->isSpace())
            return false;
        if (isNewline(*p))
            hasNewline = true;
    }
    return hasNewline;
}
bool IpeReader::atNewline() {
    return (reader.tokenType() == QXmlStreamReader::Characters)
        && isWhitespaceIncludingNewline(reader.text());
}

void IpeReader::transferToken()
{
    bool writeIsNewline = atNewline();
    if (!writeIsNewline || !lastWriteWasNewline) {
        writer.writeCurrentToken(reader);
    }
    lastWriteWasNewline = writeIsNewline;
}

void IpeReader::transferElementWithLayer(QString layerName)
{
    assert(reader.isStartElement());
    QString elementName = reader.qualifiedName().toString();

    if (layerName.isEmpty()) {
        writer.writeCurrentToken(reader);
    } else {
        if (!attr("layer").isEmpty()) {
            assert(attr("layer") == layerName);
            writer.writeCurrentToken(reader);
        } else {
            writer.writeStartElement(elementName);
            writer.writeAttributes(reader.attributes());
            writer.writeAttribute("layer", layerName);
        }
    }

    int depth = 1;
    while (!reader.atEnd()) {
        reader.readNext();
        writer.writeCurrentToken(reader);
        if (reader.isStartElement()) {
            ++depth;
        } else if (reader.isEndElement()) {
            --depth;
            if (depth == 0 && reader.qualifiedName() == elementName)
                break;
        }
    }
    lastWriteWasNewline = false;

    if (reader.atEnd())
        raiseError(QString("file ended before <%1> was closed").arg(elementName));
}

void IpeReader::readBitmapsAndStylesheets()
{
    while (!reader.atEnd()) {
        reader.readNext();
        if (reader.isStartElement()) {
            if (reader.qualifiedName() == "bitmap") {
                readBitmap();
                continue;
            } else if (reader.qualifiedName() == "ipestyle" && attr("name") == "_attributes") {
                readInstanceAndRouteAttributes();
                break;
            } else if (reader.qualifiedName() == "page") {
                break;
            }
        }
        transferToken();
    }
}

void IpeReader::readBitmap()
{
    transferToken(); // the <bitmap> element

    quint32 dataLength = attr("length").toUInt();
    int width  = attr("width").toInt();
    int height = attr("height").toInt();
    if (dataLength == 0 || width == 0 || height == 0)
        raiseError("empty Ipe image");

    QString colorSpace = attr("ColorSpace");
    QString encoding = attr("Filter");
    QImage::Format format = QImage::Format_Invalid;
    int bytesPerLine = 0;
    if (colorSpace == "DeviceGray") {
        format = QImage::Format_Indexed8;
        bytesPerLine = width;
    } else if (colorSpace == "DeviceRGB") {
        format = QImage::Format_RGB888;
        bytesPerLine = width*3;
    } else { // TODO: handle DeviceCMYK?
        raiseError(QString("unsupported Ipe image color space '%1'").arg(colorSpace));
    }

    int rawId = attr("id").toInt() - 1;
    if (rawId < 0) raiseError("invalidly numbered bitmap");
    std::size_t id = (std::size_t)rawId;
    if (images.size() <= id) {
        images.resize(id+1);
    }

    QString bitmapData = reader.readElementText();
    writer.writeCharacters(bitmapData);
    transferToken(); // </image>
    QByteArray decoded = QByteArray::fromBase64(bitmapData.toUtf8());

    if (encoding == "DCTDecode") {
        images[id] = QImage::fromData(decoded, "JPG");
    } else if (encoding == "FlateDecode") {
        // qUncompress expects the first 4 bytes of the buffer to be a big-endian, 32-bit integer
        // giving the size of the uncompressed data.
        QByteArray compressed = "0000" + decoded;
        qToBigEndian(dataLength, (uchar*)compressed.data());
        QByteArray uncompressed = qUncompress(compressed);

        // QImage's constructor doesn't automatically copy the data for us, thus causing a segfault
        // when `uncompressed` goes out of scope if we don't .copy() manually.
        images[id] = QImage((uchar const*)uncompressed.data(), width, height,
                            bytesPerLine, format).copy();
        if (format == QImage::Format_Indexed8) {
            QVector<QRgb> grayscale;
            grayscale.reserve(256);
            for (int i = 0; i < 256; ++i) {
                grayscale.append( qRgb(i, i, i) );
            }
            images[id].setColorTable(grayscale);
        }
    } else {
        raiseError("unsupported Ipe image encoding");
    }
}

void IpeReader::readInstanceAndRouteAttributes()
{
    qint64 lineNumber = reader.lineNumber();
    QString preamble = reader.readElementText(QXmlStreamReader::IncludeChildElements);
    for (QString line : preamble.split('\n')) {
        if (line.startsWith('%')) {
            int i = line.indexOf('=');
            if (i < 0) raiseError("invalid problem instance attribute", lineNumber);
            QString name = line.mid(1, i-1);
            QString value = line.mid(i+1);

            if (name == "duration") {
                bool isReal = false;
                qreal duration = value.toDouble(&isReal);
                if (!isReal)
                    raiseError(QString("invalid duration '%1'").arg(value), lineNumber);
                problemInstance.duration(duration);
            } else if (name == "labelModel") {
                auto p = LabelModel::fromName(value);
                if (!p)
                    raiseError(QString("invalid label model '%1'").arg(value), lineNumber);
                problemInstance.setLabelModel(std::move(p));
                readLabelModel = true;
            } else if (name == "labelFont") {
                QFont font;
                if (!font.fromString(value))
                    raiseError(QString("invalid label font description '%1'").arg(value), lineNumber);
                problemInstance.labelFont(font);
            } else if (name.startsWith("route")) {
                RouteParameters& params = routeParameters[name.toUtf8().data()];
                if (parseRouteParameters(params, value) < 0)
                    raiseError(QString("invalid route parameter description '%1'").arg(value), lineNumber);
            } else {
                raiseError(QString("invalid problem instance attribute '%1'").arg(name), lineNumber);
            }
        }
        ++lineNumber;
    }
    reader.readNext(); // </ipestyle>
}

void IpeReader::readFirstPage()
{
    while (reader.qualifiedName() != "page") {
        transferToken();
        reader.readNext();
        if (reader.atEnd())
            raiseError("file ended before first <page>");
    }
    transferToken(); // the <page> element

    QString currentLayer;
    bool readRoutesLayer = false;
    bool readNetworkLayer = false;
    while (!reader.atEnd()) {
        reader.readNext();
        if (!reader.isStartElement()) {
            if (reader.qualifiedName() == "page")
                break;
            transferToken();
            continue;
        }
        if (reader.qualifiedName() == "layer") {
            QString newLayer = attr("name");
            if (newLayer.isEmpty())
                raiseError("unnamed layer");
            if (currentLayer.isEmpty())
                currentLayer = newLayer;
            if (newLayer == "_routes") {
                readRoutesLayer = true;
            } else if (newLayer == "_network") {
                readNetworkLayer = true;
            }
            transferElement();
        } else {
            if (!readRoutesLayer) {
                writer.writeStartElement("layer");
                writer.writeAttribute("name", "_routes");
                writer.writeEndElement();
                readRoutesLayer = true;
            }
            if (!readNetworkLayer) {
                writer.writeStartElement("layer");
                writer.writeAttribute("name", "_network");
                writer.writeEndElement();
                readNetworkLayer = true;
            }

            QString newLayer = attr("layer");
            if (!newLayer.isEmpty())
                currentLayer = newLayer;

            if (reader.qualifiedName() == "view") {
                transferElement();
                continue;
            } else if (reader.qualifiedName() == "path") {
                if (currentLayer == "_routes") {
                    readRoute();
                    continue;
                } else if (currentLayer == "_network") {
                    readNetworkEdges();
                    continue;
                }
            } else if (reader.qualifiedName() == "image") {
                readImageReference(currentLayer);
                continue;
            } else if (reader.qualifiedName() == "page") {
                raiseError("new page started before first page ended");
            }
            transferElementWithLayer(currentLayer);
        }
    }
}

void IpeReader::readRouteOrNetworkEdges(bool isRoute)
{
    auto params = routeParameters.end();
    QString routeName = attr("dash");
    if (isRoute && !routeName.isEmpty()) {
        params = routeParameters.find(routeName.toUtf8().data());
        if (params == routeParameters.end())
            raiseError(QString("unknown route '%1'").arg(routeName));
    }

    QString rawMatrix = attr("matrix");
    QPointF matrix[3] = { QPointF(1,0), QPointF(0,1), QPointF(0,0) };
    if (!rawMatrix.isEmpty() && parseMatrix(matrix, rawMatrix) < 0) {
        raiseError("couldn't parse transformation matrix");
    }

    std::vector<VertexId> routeVertices;
    VertexId previousVertex = Graph::null_vertex();

    qint64 initialLineNumber = reader.lineNumber();
    qint64 lineNumber = initialLineNumber;
    QString pathData = reader.readElementText(QXmlStreamReader::IncludeChildElements);
    for (QString line : pathData.split('\n')) {
        QPointF pos;
        if (parsePoint(pos, line) < 0) {
            continue;
        }

        pos = ipeFlip( applyMatrix(matrix, pos) );
        VertexId vertex;
        if (!problemInstance.routeNetwork().closestVertexWithin(pos, 0.0001, vertex)) {
            vertex = problemInstance.routeNetwork().addVertex(pos);
        }
        if (isRoute) {
            routeVertices.emplace_back(vertex);
        }
        if (previousVertex != Graph::null_vertex() && vertex != previousVertex) {
            problemInstance.routeNetwork().addEdge(previousVertex, vertex);
        }

        previousVertex = vertex;
        ++lineNumber;
    }

    if (isRoute) {
        AirplaneRoute& route = problemInstance.addRoute(routeVertices);
        if (params != routeParameters.end())
            params->second.applyToRoute(route);
    }
}

void IpeReader::readImageReference(QString const& layer)
{
    bool isInt = false;
    int rawId = attr("bitmap").toInt(&isInt) - 1;
    if (!isInt || rawId < 0)
        raiseError(QString("reference to non-existent image '%1'").arg(attr("bitmap")));
    std::size_t id = (std::size_t)rawId;
    if (images.size() <= id || images[id].isNull())
        raiseError(QString("reference to non-existent image '%1'").arg(attr("bitmap")));

    QPointF rect[2];
    if (parseRect(rect, attr("rect")) < 0)
        raiseError("couldn't parse image rectangle");
    QPointF& bottomLeft = rect[0];
    QPointF& topRight   = rect[1];
    QPointF topLeft(bottomLeft.x(), topRight.y());
    QPointF bottomRight(topRight.x(), bottomLeft.y());

    QString rawMatrix = attr("matrix");
    QPointF matrix[3] = { QPointF(1,0), QPointF(0,1), QPointF(0,0) };
    if (!rawMatrix.isEmpty() && parseMatrix(matrix, rawMatrix) < 0) {
        raiseError("couldn't parse transformation matrix");
    }
    topLeft     = ipeFlip( applyMatrix(matrix, topLeft) );
    bottomRight = ipeFlip( applyMatrix(matrix, bottomRight) );

    backgroundImages.emplace_back(QRectF(topLeft, bottomRight), images[id]);

    transferElementWithLayer(layer);
}

void IpeReader::readOtherPages()
{
    while (!reader.atEnd()) {
        transferToken();
        reader.readNext();
    }
}




////////////////////////////////////////////////////////////////////////
// IPE FILE WRITING
////////////////////////////////////////////////////////////////////////

class IpeWriter {
public:
    IpeWriter(QString filePath, ProblemInstance const& problemInstance_, QByteArray const* ipeSection_)
        : file(filePath), problemInstance(problemInstance_), ipeSection(ipeSection_)
    {
        if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
            raiseError(QString("couldn't open file '%1' for writing").arg(filePath));

        writeIpeSection(0);

        writePreamble();

        writeIpeSection(1);

        writeRoutes();
        writeNetworkEdges();
        writeIsolatedNetworkVertices();

        writeIpeSection(2);
    }

private:
    QFile file;
    ProblemInstance const& problemInstance;
    QByteArray const* ipeSection;

    std::unordered_set<EdgeId, boost::hash<EdgeId> > edgesUsedInRoutes;

private:
    bool write(char const* p, qint64 length = -1);
    bool write(QByteArray const& ba) { return write(ba.constData(), ba.size()); }
    bool write(QString const& s) { return write(s.toUtf8()); }
    void writeIpeSection(int i);

    void writePreamble();

    bool writeRoute(AirplaneRoute const& route, int routeNumber);
    void writeRoutes();

    bool writeNetworkEdge(QPointF const& p1, QPointF const& p2);
    void writeNetworkEdges();

    void writeIsolatedNetworkVertices();

    template <typename String>
    void raiseError(String&& msg) {
        throw InstanceWriteError(file.fileName(), std::forward<String>(msg));
    }
    void raiseError(QString&& msg, qint64 lineNumber = -1) {
        raiseError(msg.toUtf8().data(), lineNumber);
    }
};

bool IpeWriter::write(char const* p, qint64 length)
{
    qint64 bytesRemaining = length >= 0 ? length : std::strlen(p);
    while (bytesRemaining > 0) {
        qint64 bytesWritten = file.write(p, bytesRemaining);
        if (bytesWritten < 0)
            return false;
        p += bytesWritten;
        bytesRemaining -= bytesWritten;
    }
    return true;
}

void IpeWriter::writeIpeSection(int i)
{
    if (!write(ipeSection[i])) {
        switch (i) {
        case 0: raiseError("couldn't write Ipe header"); break;
        case 1: raiseError("couldn't write first Ipe page"); break;
        case 2: raiseError("couldn't write Ipe footer"); break;
        default: assert(false);
        }
    }
}

void IpeWriter::writePreamble()
{
    if (!write("<ipestyle name=\"_attributes\">\n"
               "<preamble>\n"))
        raiseError("couldn't write problem instance parameters");

    if (!write( QString("%duration=%1\n").arg(problemInstance.duration()) ))
        raiseError("couldn't write problem instance duration");
    if (!write( QString("%labelModel=%1\n").arg(problemInstance.labelModel().name()) ))
        raiseError("couldn't write problem instance label model");
    if (!write( QString("%labelFont=%1\n").arg(problemInstance.labelFont().toString()) ))
        raiseError("couldn't write problem instance label font");

    int routeNumber = 1;
    for (auto&& route : problemInstance.routes()) {
        if (!write( QString("%route%1=%2\n").arg(routeNumber).arg(unparseRouteParameters(route)) ))
            raiseError(QString("couldn't write route #%1").arg(routeNumber));
        ++routeNumber;
    }

    if (!write("</preamble>\n"))
        raiseError("couldn't write problem instance parameters");

    routeNumber = 1;
    for (auto&& route : problemInstance.routes()) {
        Q_UNUSED(route);
        if (!write( QString("<dashstyle name=\"route%1\" value=\"[] 0\"/>\n").arg(routeNumber) ))
            raiseError(QString("couldn't write route #%1").arg(routeNumber));
        ++routeNumber;
    }

    if (!write("</ipestyle>\n"))
        raiseError("couldn't write problem instance parameters");
}

bool IpeWriter::writeRoute(const AirplaneRoute &route, int routeNumber)
{
    if (!write( QString("<path layer=\"_routes\" dash=\"route%1\">\n").arg(routeNumber) ))
        return false;

    char pathCommand = 'm';
    for (auto&& v : route.vertices()) {
        QString line = unparsePoint(v.position());
        line.append(' ');
        line.append(pathCommand);
        line.append('\n');
        if (!write(line))
            return false;
        pathCommand = 'l';
    }

    if (!write("</path>\n"))
        return false;

    return true;
}

void IpeWriter::writeRoutes()
{
    int routeNumber = 1;
    for (auto&& route : problemInstance.routes()) {
        if (!writeRoute(route, routeNumber))
            raiseError( QString("couldn't write route #%1").arg(routeNumber) );
        ++routeNumber;
    }
}

bool IpeWriter::writeNetworkEdge(QPointF const& p1, QPointF const& p2)
{
    if (!write("<path layer=\"_network\">\n"))
        return false;

    QString line = unparsePoint(p1);
    line.append(" m\n");
    if (!write(line)) return false;

    line = unparsePoint(p2);
    line.append(" l\n");
    if (!write(line)) return false;

    if (!write("</path>\n"))
        return false;

    return true;
}

void IpeWriter::writeNetworkEdges()
{
    int edgeNumber = 1;
    for (EdgeId e : problemInstance.routeNetwork().edges()) {
        if (!writeNetworkEdge(problemInstance.routeNetwork().edgeSourcePosition(e),
                              problemInstance.routeNetwork().edgeTargetPosition(e)))
            raiseError(QString("couldn't write network edge #%1").arg(edgeNumber));
    }
}

void IpeWriter::writeIsolatedNetworkVertices()
{
    int vertexNumber = 1;
    for (VertexId v : problemInstance.routeNetwork().vertices()) {
        if (!problemInstance.routeNetwork().isVertexIsolated(v))
            continue;
        QPointF position = problemInstance.routeNetwork().vertexPosition(v);
        if (!writeNetworkEdge(position, position))
            raiseError( QString("couldn't write isolated network vertex #%1").arg(vertexNumber) );
        ++vertexNumber;
    }
}

} // unnamed namespace

IpeFileData::IpeFileData(ProblemInstance& problemInstance, QString const& filePath)
    : _problemInstance(&problemInstance), _filePath(filePath)
{
    IpeReader reader(filePath, problemInstance, _ipeSection.data(), _backgroundImages);
    Q_UNUSED(reader);
}

void IpeFileData::draw(DrawingArea* drawing) const {
    for (auto&& rect_img : _backgroundImages) {
        drawing->drawImage(rect_img.first, rect_img.second);
    }
}

void IpeFileData::saveToFile(QString const& filePath)
{
    IpeWriter writer(filePath, *_problemInstance, _ipeSection.data());
    Q_UNUSED(writer);
    _filePath = filePath;
}

IpeFileData::IpeFileData(IpeFileData&& other)
    : _problemInstance(std::move(other._problemInstance))
    , _filePath(std::move(other._filePath))
    , _backgroundImages(std::move(other._backgroundImages))
{
    std::move(other._ipeSection.begin(), other._ipeSection.end(), _ipeSection.begin());
}

IpeFileData& IpeFileData::operator=(IpeFileData&& other)
{
    _problemInstance = std::move(other._problemInstance);
    _filePath = std::move(other._filePath);
    _backgroundImages = std::move(other._backgroundImages);
    std::move(other._ipeSection.begin(), other._ipeSection.end(), _ipeSection.begin());
    return *this;
}
