const fs = require("fs");
const path = require("path");

const INPUT_PATH = path.join(__dirname, "../public/map_raw.json");
const OUTPUT_PATH = path.join(__dirname, "../public/map.json");

const MESH_NAME_MAPPINGS = {
  boitesauxlettres: "boiteauxlettres",
  pyloneelectrique: "pylone",
  eoliennes: "eolienne",
  immeuble_1: "immeuble1",
  buissons: "buisson",
  panneauxcentre: "panneauclassique",
  panneauxdomaine: "panneauclassique",
  panneaudircentre: "panneaufleche",
  panneaudirdomaine: "panneaufleche",
  panneaudirfabrik: "panneaufleche",
  panneaudirresidences1: "panneaufleche",
  panneaudirresidences2: "panneaufleche",
  panneauxquartier: "panneauaffichage",
};

const IDENTITY_NODE = {
  position: [0, 0, 0],
  rotation: [0, 0, 0],
  scale: [1, 1, 1],
};
const MAX_MESH_Y_PLACEMENT_OFFSET = 2;

function cloneNode(node) {
  return {
    name: node.name,
    type: node.type,
    position: node.position,
    rotation: node.rotation,
    scale: node.scale,
  };
}

function createGroup(name, sourceNode) {
  return {
    name,
    type: "Object3D",
    role: "group",
    position: sourceNode?.position ?? IDENTITY_NODE.position,
    rotation: sourceNode?.rotation ?? IDENTITY_NODE.rotation,
    scale: sourceNode?.scale ?? IDENTITY_NODE.scale,
    children: [],
  };
}

function mapMeshNode(node) {
  return {
    ...cloneNode(node),
    name: MESH_NAME_MAPPINGS[node.name] ?? node.name,
  };
}

function getOrCreateModelGroup(parent, modelName) {
  let group = parent.children.find(
    (child) => child.name === modelName && child.type === "Object3D",
  );

  if (!group) {
    group = createGroup(modelName);
    parent.children.push(group);
  }

  return group;
}

function createRenderableObject(objectNode, meshNode) {
  const mappedMesh = mapMeshNode(meshNode);
  const renderableNode = cloneNode(objectNode ?? meshNode);

  if (objectNode && meshNode) {
    const yOffset = Math.abs(objectNode.position[1] - meshNode.position[1]);
    if (yOffset <= MAX_MESH_Y_PLACEMENT_OFFSET) {
      renderableNode.position = [
        objectNode.position[0],
        meshNode.position[1],
        objectNode.position[2],
      ];
    }
  }

  return {
    ...renderableNode,
    name: mappedMesh.name,
    type: "Object3D",
    children: [mappedMesh],
  };
}

function createRenderableContainer(objectNode, meshNodes) {
  return {
    ...cloneNode(objectNode),
    type: "Object3D",
    children: meshNodes.map(mapMeshNode),
  };
}

function addRenderable(parent, objectNode, meshNode) {
  const renderable = createRenderableObject(objectNode, meshNode);
  getOrCreateModelGroup(parent, renderable.name).children.push(renderable);
}

function addStandaloneObject(rawData, parent, name) {
  const node = rawData.find(
    (rawNode) => rawNode?.type === "Object3D" && rawNode.name === name,
  );

  if (!node) return;

  getOrCreateModelGroup(parent, name).children.push(cloneNode(node));
}

function addObjectsByRange(rawData, parent, start, end, allowedNames) {
  let currentObject = null;

  for (let i = start; i <= end; i++) {
    const node = rawData[i];

    if (node?.type === "Object3D") {
      currentObject = node;
      continue;
    }

    if (node?.type !== "Mesh") continue;
    if (allowedNames && !allowedNames.has(node.name)) continue;

    addRenderable(parent, currentObject, node);
  }
}

function addBuildingsByRange(rawData, parent, start, end) {
  for (let i = start; i <= end; i++) {
    const node = rawData[i];
    if (node?.type !== "Object3D" || node.name !== "immeuble1") continue;

    const meshNodes = [];
    for (let childIndex = i + 1; childIndex <= end; childIndex++) {
      const childNode = rawData[childIndex];

      if (childNode?.type === "Object3D") {
        if (BUILDING_CHILD_OBJECT_NAMES.has(childNode.name)) continue;
        break;
      }

      if (
        childNode?.type === "Mesh" &&
        BUILDING_MESH_NAMES.has(childNode.name)
      ) {
        meshNodes.push(childNode);
      }
    }

    if (meshNodes.length > 0) {
      getOrCreateModelGroup(parent, node.name).children.push(
        createRenderableContainer(node, meshNodes),
      );
    }
  }
}

function getNearestGroup(groups, node) {
  const [x, , z] = node.position;

  return groups.reduce((nearest, group) => {
    const [gx, , gz] = group.position;
    const distance = Math.hypot(x - gx, z - gz);
    if (!nearest || distance < nearest.distance) {
      return { group, distance };
    }
    return nearest;
  }, null).group;
}

function createResidenceZones(rawData, residence) {
  const zoneSources = [rawData[830], rawData[874], rawData[892]];
  const zones = zoneSources.map((sourceNode, index) => {
    const zone = createGroup(`zone${index + 1}_residence`, sourceNode);
    residence.children.push(zone);
    return zone;
  });

  addBuildingsByRange(rawData, zones[0], 831, 873);
  addBuildingsByRange(rawData, zones[1], 875, 891);
  addBuildingsByRange(rawData, zones[2], 893, 942);
  addObjectsByRange(rawData, zones[0], 831, 873, RESIDENCE_MESH_NAMES);
  addObjectsByRange(rawData, zones[1], 875, 891, RESIDENCE_MESH_NAMES);
  addObjectsByRange(rawData, zones[2], 893, 942, RESIDENCE_MESH_NAMES);

  for (let i = 14; i <= 23; i++) {
    const node = rawData[i];
    if (node?.type === "Mesh" && node.name === "parcebike") {
      addRenderable(getNearestGroup(zones, node), null, node);
    }
  }

  for (let i = 25; i <= 58; i++) {
    const node = rawData[i];
    if (node?.type === "Mesh" && node.name === "boitesauxlettres") {
      addRenderable(getNearestGroup(zones, node), null, node);
    }
  }

  return zones;
}

const VEGETATION_MESH_NAMES = new Set(["arbre", "sapin", "buissons"]);
const CHAMP_MESH_NAMES = new Set([
  "champdeble",
  "champdesoja",
  "champsdetournesol",
]);
const FERME_MESH_NAMES = new Set(["buissons", "buisson", "fermeverticale"]);
const RESIDENCE_MESH_NAMES = new Set(["maison1"]);
const BUILDING_CHILD_OBJECT_NAMES = new Set([
  "immeuble",
  "immeuble_1",
  "immeuble_2",
]);
const BUILDING_MESH_NAMES = new Set(["immeuble_1", "immeuble_2"]);
const ENERGIE_MESH_NAMES = new Set([
  "pyloneelectrique",
  "eoliennes",
  "panneausolaire",
  "generateur",
]);
const DIRECTION_MESH_NAMES = new Set([
  "panneauxcentre",
  "panneauxdomaine",
  "panneaudircentre",
  "panneaudirdomaine",
  "panneaudirfabrik",
  "panneaudirresidences1",
  "panneaudirresidences2",
  "panneauxquartier",
]);
const LAFABRIK_MESH_NAMES = new Set(["lafabrik", "maison1"]);
function transformMap() {
  console.log("Reading map_raw.json...");
  const rawData = JSON.parse(fs.readFileSync(INPUT_PATH, "utf-8"));

  const sceneRaw = rawData.find(
    (node) => node.name === "Scene" && node.type === "Object3D",
  );
  const terrainRaw = rawData.find(
    (node) => node.name === "terrain" && node.type === "Object3D",
  );
  const terrainMesh = rawData.find(
    (node) => node.name === "terrain" && node.type === "Mesh",
  );
  const blockingRaw = rawData.find(
    (node) => node.name === "Neutre" && node.type === "Object3D",
  );

  if (!sceneRaw || !terrainRaw || !terrainMesh || !blockingRaw) {
    throw new Error("Missing required Scene, terrain, or Neutre nodes");
  }

  const scene = createGroup("Scene", sceneRaw);
  const terrain = createGroup("terrain", terrainRaw);
  const blocking = createGroup("blocking", blockingRaw);
  const vegetation = createGroup("vegetation");
  const agriculture = createGroup("agriculture");
  const champs = createGroup("champs");
  const ferme = createGroup("ferme", rawData[4798]);
  const residence = createGroup("residence");
  const energie = createGroup("energie", rawData[4800]);
  const direction = createGroup("direction", rawData[5]);
  const lafabrik = createGroup("lafabrik", rawData[4873]);
  const ecole = createGroup("ecole", rawData[4895]);
  delete ecole.role;
  const unclassified = createGroup("unclassified");

  terrain.children.push(createRenderableObject(terrainRaw, terrainMesh));
  scene.children.push(terrain, blocking);
  blocking.children.push(
    vegetation,
    agriculture,
    residence,
    energie,
    direction,
    lafabrik,
    ecole,
  );
  agriculture.children.push(champs, ferme);

  addObjectsByRange(rawData, direction, 6, 12, DIRECTION_MESH_NAMES);
  addStandaloneObject(rawData, residence, "ebike");
  createResidenceZones(rawData, residence);
  addObjectsByRange(rawData, energie, 61, 96, new Set(["pyloneelectrique"]));
  addObjectsByRange(rawData, vegetation, 98, 829, VEGETATION_MESH_NAMES);
  addObjectsByRange(rawData, agriculture, 944, 944, new Set(["tuyauxlac"]));
  addObjectsByRange(rawData, champs, 946, 4594, CHAMP_MESH_NAMES);
  addObjectsByRange(rawData, ferme, 4595, 4799, FERME_MESH_NAMES);
  addObjectsByRange(rawData, vegetation, 4750, 4797, VEGETATION_MESH_NAMES);
  addObjectsByRange(rawData, energie, 4801, 4872, ENERGIE_MESH_NAMES);
  addBuildingsByRange(rawData, lafabrik, 4874, 4894);
  addObjectsByRange(rawData, lafabrik, 4874, 4894, LAFABRIK_MESH_NAMES);
  addObjectsByRange(rawData, direction, 4896, 4897, DIRECTION_MESH_NAMES);
  addObjectsByRange(rawData, vegetation, 4898, 4997, VEGETATION_MESH_NAMES);

  for (let i = 0; i < rawData.length; i++) {
    const node = rawData[i];
    if (node.type !== "Mesh") continue;
    if (node.name === "mc") continue;
    if (node.name === "bati-ecole") continue;

    const alreadyClassified = isMeshClassified(scene, node);
    if (!alreadyClassified) {
      addRenderable(unclassified, null, node);
    }
  }

  if (unclassified.children.length > 0) {
    blocking.children.push(unclassified);
  }

  fs.writeFileSync(OUTPUT_PATH, JSON.stringify(scene, null, 2));
  console.log(`Written hierarchical map to ${OUTPUT_PATH}`);
}

function isSameTransform(a, b) {
  return (
    a.name === (MESH_NAME_MAPPINGS[b.name] ?? b.name) &&
    JSON.stringify(a.position) === JSON.stringify(b.position) &&
    JSON.stringify(a.rotation) === JSON.stringify(b.rotation) &&
    JSON.stringify(a.scale) === JSON.stringify(b.scale)
  );
}

function isMeshClassified(root, rawMeshNode) {
  const stack = [...(root.children ?? [])];

  while (stack.length > 0) {
    const node = stack.pop();
    if (node.type === "Mesh" && isSameTransform(node, rawMeshNode)) {
      return true;
    }
    stack.push(...(node.children ?? []));
  }

  return false;
}

transformMap();
