/* rbxBinaryParser (XML-compatible shim)
 *
 * Drop-in replacement for rbxbinaryparser.decode(buffer)
 * - Binary files: throws (stub – plug real parser if you want)
 * - XML files (.rbxlx / .rbxmx): fully supported
 */

/* =========================
 * Public API
 * ========================= */

export function decode(buffer) {
  if (isXmlRoblox(buffer)) {
    return decodeXml(buffer);
  }

  throw new Error(
    "Binary RBXM/RBXL not supported in this build. " +
    "This file is an XML-compatible rbxbinaryparser shim."
  );
}

/* =========================
 * Format detection
 * ========================= */

function isXmlRoblox(buffer) {
  const u8 = new Uint8Array(buffer, 0, Math.min(64, buffer.byteLength));
  const text = new TextDecoder("utf-8").decode(u8).trimStart();
  return text.startsWith("<roblox");
}

/* =========================
 * XML decoding
 * ========================= */

function decodeXml(buffer) {
  const xmlText = new TextDecoder("utf-8").decode(buffer);
  const dom = new DOMParser().parseFromString(xmlText, "text/xml");

  const roblox = dom.documentElement;
  if (!roblox || roblox.tagName !== "roblox") {
    throw new Error("Invalid RBXLX/RBXMX file");
  }

  const referentMap = new Map();
  const roots = [];

  for (const child of roblox.children) {
    if (child.tagName === "Item") {
      roots.push(parseItem(child, referentMap));
    }
  }

  // Resolve Ref properties after tree is built
  for (const root of roots) {
    resolveReferences(root, referentMap);
  }

  return roots;
}

/* =========================
 * Item parsing
 * ========================= */

function parseItem(itemNode, referentMap) {
  const inst = {
    ClassName: itemNode.getAttribute("class"),
    Properties: {},
    Children: []
  };

  const referent = itemNode.getAttribute("referent");
  if (referent) {
    referentMap.set(referent, inst);
  }

  for (const child of itemNode.children) {
    if (child.tagName === "Properties") {
      parseProperties(child, inst.Properties);
    } else if (child.tagName === "Item") {
      inst.Children.push(parseItem(child, referentMap));
    }
  }

  return inst;
}

/* =========================
 * Properties
 * ========================= */

function parseProperties(propsNode, out) {
  for (const prop of propsNode.children) {
    const name = prop.getAttribute("name");
    out[name] = parsePropertyValue(prop);
  }
}

/* =========================
 * Property decoding
 * ========================= */

function parsePropertyValue(el) {
  switch (el.tagName) {
    /* ---- primitives ---- */

    case "string":
    case "ProtectedString":
      return el.textContent ?? "";

    case "bool":
      return el.textContent.trim() === "true";

    case "int":
    case "int64":
      return parseInt(el.textContent.trim(), 10);

    case "float":
    case "double":
      return parseFloat(el.textContent.trim());

    /* ---- vectors ---- */

    case "Vector2":
      return {
        X: floatChild(el, "X"),
        Y: floatChild(el, "Y"),
      };

    case "Vector3":
      return {
        X: floatChild(el, "X"),
        Y: floatChild(el, "Y"),
        Z: floatChild(el, "Z"),
      };

    case "Vector2int16":
      return {
        X: intChild(el, "X"),
        Y: intChild(el, "Y"),
      };

    case "Vector3int16":
      return {
        X: intChild(el, "X"),
        Y: intChild(el, "Y"),
        Z: intChild(el, "Z"),
      };

    /* ---- UDim ---- */

    case "UDim":
      return {
        Scale: floatChild(el, "Scale"),
        Offset: intChild(el, "Offset"),
      };

    case "UDim2":
      return {
        X: parsePropertyValue(el.querySelector("X")),
        Y: parsePropertyValue(el.querySelector("Y")),
      };

    /* ---- CFrame ---- */

    case "CoordinateFrame": {
      const X = floatChild(el, "X");
      const Y = floatChild(el, "Y");
      const Z = floatChild(el, "Z");

      const rot = [
        floatChild(el, "R00"),
        floatChild(el, "R01"),
        floatChild(el, "R02"),
        floatChild(el, "R10"),
        floatChild(el, "R11"),
        floatChild(el, "R12"),
        floatChild(el, "R20"),
        floatChild(el, "R21"),
        floatChild(el, "R22"),
      ];

      return {
        Position: { X, Y, Z },
        Components: [X, Y, Z, ...rot],
      };
    }

    /* ---- BrickColor ---- */

    case "BrickColor":
      return { Number: parseInt(el.textContent.trim(), 10) };

    /* ---- Color3 ---- */

    case "Color3": {
      if (el.children.length === 0) {
        const n = parseInt(el.textContent.trim(), 10);
        return {
          R: ((n >> 16) & 0xff) / 255,
          G: ((n >> 8) & 0xff) / 255,
          B: (n & 0xff) / 255,
        };
      }
      return {
        R: floatChild(el, "R"),
        G: floatChild(el, "G"),
        B: floatChild(el, "B"),
      };
    }

    case "Color3uint8": {
      const n = parseInt(el.textContent.trim(), 10);
      return {
        R: ((n >> 16) & 0xff) / 255,
        G: ((n >> 8) & 0xff) / 255,
        B: (n & 0xff) / 255,
      };
    }

    /* ---- Sequences ---- */

    case "NumberSequence": {
      const nums = el.textContent.trim().split(/\s+/).map(Number);
      const out = [];
      for (let i = 0; i < nums.length; i += 3) {
        out.push({
          Time: nums[i],
          Value: nums[i + 1],
          Envelope: nums[i + 2],
        });
      }
      return out;
    }

    case "ColorSequence": {
      const nums = el.textContent.trim().split(/\s+/).map(Number);
      const out = [];
      for (let i = 0; i < nums.length; i += 5) {
        out.push({
          Time: nums[i],
          Value: {
            R: nums[i + 1],
            G: nums[i + 2],
            B: nums[i + 3],
          },
          Envelope: nums[i + 4],
        });
      }
      return out;
    }

    /* ---- Faces / Axes ---- */

    case "Faces":
      return decodeBits(el, ["Right", "Top", "Back", "Left", "Bottom", "Front"]);

    case "Axes":
      return decodeBits(el, ["X", "Y", "Z"]);

    /* ---- Ray ---- */

    case "Ray":
      return {
        Origin: parsePropertyValue(el.querySelector("origin")),
        Direction: parsePropertyValue(el.querySelector("direction")),
      };

    /* ---- References ---- */

    case "Ref":
      return { __ref__: el.textContent.trim() };

    /* ---- Content ---- */

    case "Content": {
      const url = el.querySelector("url");
      return url ? url.textContent : "";
    }

    /* ---- Fallback ---- */

    default:
      return el.textContent ?? null;
  }
}

/* =========================
 * Helpers
 * ========================= */

function floatChild(el, name) {
  const c = el.querySelector(name);
  return c ? parseFloat(c.textContent.trim()) : 0;
}

function intChild(el, name) {
  const c = el.querySelector(name);
  return c ? parseInt(c.textContent.trim(), 10) : 0;
}

function decodeBits(el, names) {
  const v = parseInt(el.textContent.trim(), 10);
  const out = {};
  for (let i = 0; i < names.length; i++) {
    out[names[i]] = !!(v & (1 << i));
  }
  return out;
}

/* =========================
 * Reference resolution
 * ========================= */

function resolveReferences(inst, referentMap) {
  for (const key in inst.Properties) {
    const v = inst.Properties[key];
    if (v && typeof v === "object" && v.__ref__) {
      inst.Properties[key] = referentMap.get(v.__ref__) || null;
    }
  }
  for (const c of inst.Children) {
    resolveReferences(c, referentMap);
  }
}
