#include "io/LayoutXml.h"
#include "model/PlacedScenery.h"
#include <fstream>
#include <sstream>
#include <iomanip>
#include <set>

// Minimal XML writer/reader — no external dependencies

namespace TD {

// ── Helpers ───────────────────────────────────────────────────────────────────
static std::string esc(const std::string& s) {
    std::string r;
    for (char c : s) {
        if      (c == '<')  r += "&lt;";
        else if (c == '>')  r += "&gt;";
        else if (c == '&')  r += "&amp;";
        else if (c == '"')  r += "&quot;";
        else                r += c;
    }
    return r;
}

static std::string attr(const std::string& k, const std::string& v) {
    return " " + k + "=\"" + esc(v) + "\"";
}
static std::string attr(const std::string& k, double v) {
    std::ostringstream ss; ss << std::fixed << std::setprecision(4) << v;
    return attr(k, ss.str());
}
static std::string attr(const std::string& k, int v) {
    return attr(k, std::to_string(v));
}

// Simple XML value extractor
static std::string xmlAttr(const std::string& line, const std::string& key) {
    std::string search = key + "=\"";
    auto pos = line.find(search);
    if (pos == std::string::npos) return "";
    pos += search.size();
    auto end = line.find('"', pos);
    if (end == std::string::npos) return "";
    return line.substr(pos, end - pos);
}

// ── Write pieces ──────────────────────────────────────────────────────────────
static void WritePieces(std::ostream& f,
                        const std::vector<MC::PlacedPiece>& pieces,
                        const std::string& indent)
{
    for (const auto& p : pieces) {
        f << indent << "<piece"
          << attr("id",      p.id)
          << attr("cat",     p.catalogNumber)
          << attr("x",       p.transform.pos.x)
          << attr("y",       p.transform.pos.y)
          << attr("heading", p.transform.heading)
          << attr("z",       p.z_mm)
          << attr("grade",   p.gradeDeg)
          << (p.chainReversed ? attr("rev", 1) : "")
          << (p.freeWithTurntable ? attr("free", 1) : "")
          << attr("level",   p.levelId)
          << (p.helixId != -1 ? attr("helix", p.helixId) : "")
          << attr("sel",     p.selected ? 1 : 0)
          << (p.hasColor ? attr("color", (int)p.colorR<<16|(int)p.colorG<<8|(int)p.colorB) : "")
          << ">\n";
        for (int i = 0; i < (int)p.joints.size(); ++i) {
            if (p.joints[i].pieceId != -1) {
                f << indent << "  <joint"
                  << attr("conn",   i)
                  << attr("target", p.joints[i].pieceId)
                  << attr("tconn",  p.joints[i].connIdx)
                  << "/>\n";
            }
        }
        f << indent << "</piece>\n";
    }
}

// ── Read pieces ───────────────────────────────────────────────────────────────
static void ReadPieces(std::istream& in,
                       std::vector<MC::PlacedPiece>& pieces,
                       int& maxId)
{
    std::string line;
    MC::PlacedPiece* cur = nullptr;

    while (std::getline(in, line)) {
        if (line.find("</pieces>") != std::string::npos ||
            line.find("</block>")  != std::string::npos ||
            line.find("</layout>") != std::string::npos) break;

        if (line.find("<piece ") != std::string::npos) {
            MC::PlacedPiece p;
            p.id                  = std::stoi(xmlAttr(line, "id"));
            p.catalogNumber       = xmlAttr(line, "cat");
            p.transform.pos.x     = std::stod(xmlAttr(line, "x"));
            p.transform.pos.y     = std::stod(xmlAttr(line, "y"));
            p.transform.heading   = std::stod(xmlAttr(line, "heading"));
            p.selected            = xmlAttr(line, "sel") == "1";
            std::string zStr      = xmlAttr(line, "z");
            p.z_mm                = zStr.empty() ? 0.0 : std::stod(zStr);
            std::string grStr     = xmlAttr(line, "grade");
            p.gradeDeg            = grStr.empty() ? 0.0 : std::stod(grStr);
            std::string revStr    = xmlAttr(line, "rev");
            p.chainReversed       = (revStr == "1");
            std::string freeStr   = xmlAttr(line, "free");
            p.freeWithTurntable   = (freeStr == "1");
            std::string colStr    = xmlAttr(line, "color");
            if (!colStr.empty()) {
                int rgb = std::stoi(colStr);
                p.colorR = (rgb >> 16) & 0xFF;
                p.colorG = (rgb >>  8) & 0xFF;
                p.colorB =  rgb        & 0xFF;
                p.hasColor = true;
            }
            std::string lvlStr    = xmlAttr(line, "level");
            p.levelId             = lvlStr.empty() ? 1 : std::stoi(lvlStr);
            std::string hxStr     = xmlAttr(line, "helix");
            p.helixId             = hxStr.empty() ? -1 : std::stoi(hxStr);
            maxId = std::max(maxId, p.id);
            pieces.push_back(std::move(p));
            cur = &pieces.back();
        } else if (line.find("<joint ") != std::string::npos && cur) {
            int conn   = std::stoi(xmlAttr(line, "conn"));
            int target = std::stoi(xmlAttr(line, "target"));
            int tconn  = std::stoi(xmlAttr(line, "tconn"));
            if (conn >= (int)cur->joints.size())
                cur->joints.resize(conn + 1, {-1, -1});
            cur->joints[conn] = { target, tconn };
        }
    }
}

// ── Save ──────────────────────────────────────────────────────────────────────
std::string LayoutXml::Save(const MC::CanvasState& state,
                             const std::string& filePath,
                             const std::string& layoutName)
{
    std::ofstream f(filePath);
    if (!f) return "Cannot open file: " + filePath;

    f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    f << "<layout" << attr("name", layoutName)
      << attr("version", "1")
      << attr("units",   state.units)
      << ">\n";

    // Save grade runs
    if (!state.gradeRuns.empty()) {
        f << "  <grade_runs>\n";
        for (const auto& r : state.gradeRuns)
            f << "    <run" << attr("deg", r.gradeDeg)
              << attr("anchor", r.anchorId)
              << attr("conn",   r.connIdx) << "/>\n";
        f << "  </grade_runs>\n";
    }
    f << "  <pieces>\n";
    WritePieces(f, state.pieces, "    ");
    f << "  </pieces>\n";

    // Scenery
    if (!state.scenery.empty()) {
        f << "  <scenery>\n";
        for (const auto& s : state.scenery) {
            f << "    <sceneryitem"
              << attr("id",      s.id)
              << attr("item",    s.itemId)
              << attr("x",       s.transform.pos.x)
              << attr("y",       s.transform.pos.y)
              << attr("heading", s.transform.heading)
              << attr("scalex",  s.scaleX)
              << attr("scaley",  s.scaleY)
              << attr("level",   s.levelId)
              << attr("sel",     s.selected ? 1 : 0)
              << "/>\n";
        }
        f << "  </scenery>\n";
    }

    // Levels
    if (!state.levels.empty()) {
        f << "  <levels active=\"" << state.activeLevelId << "\">\n";
        for (const auto& l : state.levels) {
            f << "    <level"
              << attr("id",      l.id)
              << attr("name",    l.name)
              << attr("visible", l.visible ? 1 : 0)
              << attr("height",  l.heightMm)
              << attr("trackcolor", l.hasTrackColor ? 1 : 0)
              << attr("r",       (int)l.r)
              << attr("g",       (int)l.g)
              << attr("b",       (int)l.b);
            if (l.shape.size() >= 3) {
                f << ">\n";
                for (const auto& v : l.shape)
                    f << "      <pt" << attr("x", v.x) << attr("y", v.y) << "/>\n";
                f << "    </level>\n";
            } else {
                f << "/>\n";
            }
        }
        f << "  </levels>\n";
    }

    // Baseboard polygon
    if (!state.baseboard.empty()) {
        f << "  <baseboard>\n";
        for (const auto& v : state.baseboard)
            f << "    <vertex" << attr("x", v.x) << attr("y", v.y) << "/>\n";
        f << "  </baseboard>\n";
    }

    // Layout-shape dialog memory (last selected shape + dimensions).
    f << "  <lastshape"
      << attr("id",    state.lastShape.shapeId)
      << attr("w_mm",  state.lastShape.w_mm)
      << attr("h_mm",  state.lastShape.h_mm)
      << attr("w2_mm", state.lastShape.w2_mm)
      << attr("h2_mm", state.lastShape.h2_mm)
      << "/>\n";

    // Labels
    if (!state.labels.empty()) {
        f << "  <labels>\n";
        for (const auto& l : state.labels)
            f << "    <label" << attr("id", l.id)
              << attr("x", l.pos.x) << attr("y", l.pos.y)
              << attr("text", l.text) << "/>\n";
        f << "  </labels>\n";
    }

    // Free-form polygon shapes (access hatches, mountains, rivers...)
    if (!state.shapes.empty()) {
        f << "  <shapes>\n";
        for (const auto& s : state.shapes) {
            f << "    <shape"
              << attr("id", s.id)
              << attr("r", (int)s.r) << attr("g", (int)s.g) << attr("b", (int)s.b)
              << attr("a", (int)s.alpha)
              << attr("outline", s.hasOutline ? 1 : 0)
              << attr("level",   s.levelId)
              << attr("height",  s.heightMm)
              << attr("label",   s.label)
              << ">\n";
            for (const auto& v : s.vertices)
                f << "      <v" << attr("x", v.x) << attr("y", v.y) << "/>\n";
            f << "    </shape>\n";
        }
        f << "  </shapes>\n";
    }

    // Tunnels: portal pair + covered piece list, recorded explicitly.
    if (!state.tunnels.empty()) {
        f << "  <tunnels>\n";
        for (const auto& t : state.tunnels) {
            f << "    <tunnel" << attr("id", t.id)
              << attr("portalA", t.portalIdA)
              << attr("portalB", t.portalIdB)
              << attr("anchorA", t.anchorPieceA)
              << attr("anchorB", t.anchorPieceB) << ">\n";
            for (int pid : t.coveredPieceIds) {
                f << "      <covers piece=\"" << pid << "\"/>\n";
            }
            f << "    </tunnel>\n";
        }
        f << "  </tunnels>\n";
    }

    // Helices: monolithic two-track spiral units. Internals computed at render
    // time from center/heading; we only persist the parameters and stub IDs.
    if (!state.helices.empty()) {
        f << "  <helices>\n";
        for (const auto& h : state.helices) {
            f << "    <helix" << attr("id", h.id)
              << attr("cx", h.center.x) << attr("cy", h.center.y)
              << attr("heading", h.heading)
              << attr("variant", h.variantId)
              << attr("bottomR1", h.bottomStubR1)
              << attr("bottomR2", h.bottomStubR2)
              << attr("topR1",    h.topStubR1)
              << attr("topR2",    h.topStubR2) << "/>\n";
        }
        f << "  </helices>\n";
    }

    f << "</layout>\n";
    return "";
}

// ── Load ──────────────────────────────────────────────────────────────────────
std::string LayoutXml::Load(MC::CanvasState& state,
                             const std::string& filePath,
                             std::string& layoutNameOut)
{
    std::ifstream f(filePath);
    if (!f) return "Cannot open file: " + filePath;

    state.pieces.clear();
    state.scenery.clear();
    state.labels.clear();
    state.shapes.clear();
    state.levels.clear();
    state.tunnels.clear();
    state.helices.clear();
    layoutNameOut.clear();
    int maxId = 0;

    std::string line;
    bool inScenery   = false;
    bool inBaseboard = false;
    bool inLevels    = false;
    while (std::getline(f, line)) {
        if (line.find("<layout ") != std::string::npos) {
            layoutNameOut = xmlAttr(line, "name");
            std::string uStr = xmlAttr(line, "units");
            state.units = uStr.empty() ? 0 : std::stoi(uStr);
        }
        if (line.find("<run ") != std::string::npos) {
            MC::CanvasState::GradeRun r;
            r.gradeDeg = std::stod(xmlAttr(line, "deg"));
            r.anchorId = std::stoi(xmlAttr(line, "anchor"));
            r.connIdx  = std::stoi(xmlAttr(line, "conn"));
            state.gradeRuns.push_back(r);
        }
        if (line.find("<pieces>") != std::string::npos)
            ReadPieces(f, state.pieces, maxId);
        if (line.find("<levels ") != std::string::npos) {
            inLevels = true;
            std::string aStr = xmlAttr(line, "active");
            state.activeLevelId = aStr.empty() ? 1 : std::stoi(aStr);
        }
        if (line.find("</levels>") != std::string::npos)
            inLevels = false;
        if (inLevels && line.find("<level ") != std::string::npos) {
            MC::Level l;
            l.id      = std::stoi(xmlAttr(line, "id"));
            l.name    = xmlAttr(line, "name");
            l.visible = xmlAttr(line, "visible") != "0";
            std::string hStr = xmlAttr(line, "height");
            l.heightMm = hStr.empty() ? 0.0 : std::stod(hStr);
            l.hasTrackColor = xmlAttr(line, "trackcolor") == "1";
            std::string rStr = xmlAttr(line, "r"), gStr = xmlAttr(line, "g"), bStr = xmlAttr(line, "b");
            l.r = rStr.empty() ? 180 : (uint8_t)std::stoi(rStr);
            l.g = gStr.empty() ? 180 : (uint8_t)std::stoi(gStr);
            l.b = bStr.empty() ? 180 : (uint8_t)std::stoi(bStr);
            state.levels.push_back(l);
        }
        // Per-level deck shape points (nested <pt> inside an open <level>).
        if (inLevels && line.find("<pt ") != std::string::npos
            && !state.levels.empty()) {
            MC::Vec2 v;
            v.x = std::stod(xmlAttr(line, "x"));
            v.y = std::stod(xmlAttr(line, "y"));
            state.levels.back().shape.push_back(v);
        }
        if (line.find("<scenery>") != std::string::npos)
            inScenery = true;
        if (line.find("</scenery>") != std::string::npos)
            inScenery = false;
        if (line.find("<baseboard>") != std::string::npos) {
            inBaseboard = true;
            state.baseboard.clear();
        }
        if (line.find("</baseboard>") != std::string::npos)
            inBaseboard = false;
        if (inBaseboard && line.find("<vertex ") != std::string::npos) {
            MC::Vec2 v;
            v.x = std::stod(xmlAttr(line, "x"));
            v.y = std::stod(xmlAttr(line, "y"));
            state.baseboard.push_back(v);
        }
        if (line.find("<lastshape") != std::string::npos) {
            std::string s;
            s = xmlAttr(line, "id");    if (!s.empty()) state.lastShape.shapeId = std::stoi(s);
            s = xmlAttr(line, "w_mm");  if (!s.empty()) state.lastShape.w_mm    = std::stod(s);
            s = xmlAttr(line, "h_mm");  if (!s.empty()) state.lastShape.h_mm    = std::stod(s);
            s = xmlAttr(line, "w2_mm"); if (!s.empty()) state.lastShape.w2_mm   = std::stod(s);
            s = xmlAttr(line, "h2_mm"); if (!s.empty()) state.lastShape.h2_mm   = std::stod(s);
        }
        if (inScenery && line.find("<sceneryitem ") != std::string::npos) {
            MC::PlacedScenery s;
            s.id                  = std::stoi(xmlAttr(line, "id"));
            s.itemId              = xmlAttr(line, "item");
            s.transform.pos.x     = std::stod(xmlAttr(line, "x"));
            s.transform.pos.y     = std::stod(xmlAttr(line, "y"));
            s.transform.heading   = std::stod(xmlAttr(line, "heading"));
            s.selected            = xmlAttr(line, "sel") == "1";
            std::string sxStr     = xmlAttr(line, "scalex");
            std::string syStr     = xmlAttr(line, "scaley");
            s.scaleX              = sxStr.empty() ? 1.0 : std::stod(sxStr);
            s.scaleY              = syStr.empty() ? 1.0 : std::stod(syStr);
            std::string slvlStr   = xmlAttr(line, "level");
            s.levelId             = slvlStr.empty() ? 1 : std::stoi(slvlStr);
            maxId = std::max(maxId, s.id);
            state.scenery.push_back(std::move(s));
        }

        if (line.find("<label ") != std::string::npos) {
            MC::CanvasLabel l;
            l.id    = std::stoi(xmlAttr(line, "id"));
            l.pos.x = std::stod(xmlAttr(line, "x"));
            l.pos.y = std::stod(xmlAttr(line, "y"));
            l.text  = xmlAttr(line, "text");
            maxId   = std::max(maxId, l.id);
            state.labels.push_back(std::move(l));
        }

        // Free-form polygon shapes — <shape ...> open, <v x y/> vertices,
        // </shape> close. We stream them: when we see <shape  open a record;
        // when we see <v inside one, append a vertex; when we see </shape>
        // commit it (no-op since we built in place).
        //
        // Distinguish "<shape " (real opener) from "<shapes>" (collection
        // wrapper) and "</shape" (closer) by requiring a SPACE after the tag
        // name. The collection opener is "<shapes>" with no space; the real
        // opener is "<shape id=...".
        if (line.find("<shape ") != std::string::npos &&
            line.find("</shape") == std::string::npos)
        {
            MC::CanvasShape s;
            std::string idStr = xmlAttr(line, "id");
            // Guard against malformed lines that lack an id attribute
            // (defensive: shouldn't happen on files we wrote).
            if (idStr.empty()) continue;
            s.id = std::stoi(idStr);
            std::string t;
            t = xmlAttr(line, "r");        if (!t.empty()) s.r = (uint8_t)std::stoi(t);
            t = xmlAttr(line, "g");        if (!t.empty()) s.g = (uint8_t)std::stoi(t);
            t = xmlAttr(line, "b");        if (!t.empty()) s.b = (uint8_t)std::stoi(t);
            t = xmlAttr(line, "a");        if (!t.empty()) s.alpha = (uint8_t)std::stoi(t);
            t = xmlAttr(line, "outline");  if (!t.empty()) s.hasOutline = (t == "1");
            t = xmlAttr(line, "level");    if (!t.empty()) s.levelId = std::stoi(t);
            t = xmlAttr(line, "height");   if (!t.empty()) s.heightMm = std::stod(t);
            s.label = xmlAttr(line, "label");
            maxId = std::max(maxId, s.id);
            state.shapes.push_back(std::move(s));
        }
        if (line.find("<v ") != std::string::npos && !state.shapes.empty()) {
            // Heuristic: a <v x= y=/> line inside a shape block. Distinguish
            // from any other <v tag by requiring x and y attrs.
            std::string xs = xmlAttr(line, "x");
            std::string ys = xmlAttr(line, "y");
            if (!xs.empty() && !ys.empty()) {
                MC::Vec2 v;
                v.x = std::stod(xs);
                v.y = std::stod(ys);
                state.shapes.back().vertices.push_back(v);
            }
        }

        // Tunnels: open a record on <tunnel ...>, close on </tunnel>, collect
        // covered piece ids from <covers piece="N"/> lines in between.
        if (line.find("<tunnel ") != std::string::npos) {
            MC::Tunnel t;
            t.id        = std::stoi(xmlAttr(line, "id"));
            t.portalIdA = std::stoi(xmlAttr(line, "portalA"));
            t.portalIdB = std::stoi(xmlAttr(line, "portalB"));
            std::string aStr = xmlAttr(line, "anchorA");
            std::string bStr = xmlAttr(line, "anchorB");
            t.anchorPieceA = aStr.empty() ? -1 : std::stoi(aStr);
            t.anchorPieceB = bStr.empty() ? -1 : std::stoi(bStr);
            maxId = std::max(maxId, t.id);
            state.tunnels.push_back(std::move(t));
        }
        if (line.find("<covers ") != std::string::npos && !state.tunnels.empty()) {
            int pid = std::stoi(xmlAttr(line, "piece"));
            state.tunnels.back().coveredPieceIds.push_back(pid);
        }
        // Helix: a single self-closing tag with all parameters.
        if (line.find("<helix ") != std::string::npos) {
            MC::Helix h;
            h.id      = std::stoi(xmlAttr(line, "id"));
            h.center.x = std::stod(xmlAttr(line, "cx"));
            h.center.y = std::stod(xmlAttr(line, "cy"));
            h.heading = std::stod(xmlAttr(line, "heading"));
            // Variant: missing attribute (pre-variant save) → default 2.5.
            std::string vStr = xmlAttr(line, "variant");
            h.variantId = vStr.empty() ? "proses_2_5_r1r2" : vStr;
            h.bottomStubR1 = std::stoi(xmlAttr(line, "bottomR1"));
            h.bottomStubR2 = std::stoi(xmlAttr(line, "bottomR2"));
            h.topStubR1    = std::stoi(xmlAttr(line, "topR1"));
            h.topStubR2    = std::stoi(xmlAttr(line, "topR2"));
            maxId = std::max(maxId, h.id);
            state.helices.push_back(std::move(h));
        }
    }

    state.nextId = maxId + 1;
    state.EnsureDefaultLevel();
    state.SyncGradeFromRuns();

    // A helix must always be selected as a unit. If a saved selection has
    // only some of a helix's stubs, expand it now so any subsequent move
    // operates on the whole helix.
    {
        std::set<int> touched;
        for (const auto& p : state.pieces)
            if (p.selected && p.helixId != -1)
                touched.insert(p.helixId);
        if (!touched.empty()) {
            for (auto& p : state.pieces)
                if (p.helixId != -1 && touched.count(p.helixId))
                    p.selected = true;
        }
    }
    return "";
}

// ── SaveBlock ─────────────────────────────────────────────────────────────────
std::string LayoutXml::SaveBlock(MC::CanvasState& state,
                                  const std::string& filePath,
                                  const std::string& blockName)
{
    auto selected      = state.Selected();
    auto selectedScene = state.SelectedScenery();
    if (selected.empty() && selectedScene.empty())
        return "No pieces or scenery selected";

    std::ofstream f(filePath);
    if (!f) return "Cannot open file: " + filePath;

    f << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    f << "<block" << attr("name", blockName) << attr("version", "1") << ">\n";

    // Write selected pieces
    if (!selected.empty()) {
        std::vector<MC::PlacedPiece> sel;
        for (auto* p : selected) sel.push_back(*p);
        WritePieces(f, sel, "  ");
    }

    // Write selected scenery (same format as main layout save)
    if (!selectedScene.empty()) {
        f << "  <scenery>\n";
        for (auto* s : selectedScene) {
            f << "    <sceneryitem"
              << attr("id",      s->id)
              << attr("item",    s->itemId)
              << attr("x",       s->transform.pos.x)
              << attr("y",       s->transform.pos.y)
              << attr("heading", s->transform.heading)
              << attr("scalex",  s->scaleX)
              << attr("scaley",  s->scaleY)
              << attr("level",   s->levelId)
              << attr("sel",     0)
              << "/>\n";
        }
        f << "  </scenery>\n";
    }

    f << "</block>\n";
    return "";
}

// ── LoadBlock ─────────────────────────────────────────────────────────────────
std::string LayoutXml::LoadBlock(const std::string& filePath,
                                  std::string& blockNameOut,
                                  std::vector<MC::PlacedPiece>& piecesOut,
                                  std::vector<MC::PlacedScenery>& sceneryOut)
{
    std::ifstream f(filePath);
    if (!f) return "Cannot open file: " + filePath;

    blockNameOut.clear();
    piecesOut.clear();
    sceneryOut.clear();
    int maxId = 0;
    bool inScenery = false;

    std::string line;
    while (std::getline(f, line)) {
        if (line.find("<block ") != std::string::npos)
            blockNameOut = xmlAttr(line, "name");

        if (line.find("<scenery>")  != std::string::npos) inScenery = true;
        if (line.find("</scenery>") != std::string::npos) inScenery = false;

        if (inScenery && line.find("<sceneryitem ") != std::string::npos) {
            MC::PlacedScenery s;
            s.id                = std::stoi(xmlAttr(line, "id"));
            s.itemId            = xmlAttr(line, "item");
            s.transform.pos.x   = std::stod(xmlAttr(line, "x"));
            s.transform.pos.y   = std::stod(xmlAttr(line, "y"));
            s.transform.heading = std::stod(xmlAttr(line, "heading"));
            std::string sxStr   = xmlAttr(line, "scalex");
            std::string syStr   = xmlAttr(line, "scaley");
            s.scaleX            = sxStr.empty() ? 1.0 : std::stod(sxStr);
            s.scaleY            = syStr.empty() ? 1.0 : std::stod(syStr);
            std::string slvlStr = xmlAttr(line, "level");
            s.levelId           = slvlStr.empty() ? 1 : std::stoi(slvlStr);
            maxId = std::max(maxId, s.id);
            sceneryOut.push_back(std::move(s));
            continue;
        }

        if (line.find("<piece ") != std::string::npos) {
            MC::PlacedPiece p;
            p.id                = std::stoi(xmlAttr(line, "id"));
            p.catalogNumber     = xmlAttr(line, "cat");
            p.transform.pos.x   = std::stod(xmlAttr(line, "x"));
            p.transform.pos.y   = std::stod(xmlAttr(line, "y"));
            p.transform.heading = std::stod(xmlAttr(line, "heading"));
            std::string lvlStr  = xmlAttr(line, "level");
            p.levelId           = lvlStr.empty() ? 1 : std::stoi(lvlStr);
            maxId = std::max(maxId, p.id);
            piecesOut.push_back(std::move(p));
        } else if (line.find("<joint ") != std::string::npos && !piecesOut.empty()) {
            int conn   = std::stoi(xmlAttr(line, "conn"));
            int target = std::stoi(xmlAttr(line, "target"));
            int tconn  = std::stoi(xmlAttr(line, "tconn"));
            auto& cur = piecesOut.back();
            if (conn >= (int)cur.joints.size())
                cur.joints.resize(conn + 1, {-1, -1});
            cur.joints[conn] = { target, tconn };
        }
    }
    return "";
}

} // namespace TD
