Source: join.js

import { isgeojson } from "./helpers/helpers.js";
import { combine } from "./combine.js";

/**
 * @function join
 * @summary Join datasets using a common identifier.
 * @param {array} data - An array of datsats and/or geoJSONs. The join operation is basend on the first item.
 * @param {object} options - Options
 * @param {array|string} [options.ids] - An array of code of the same size. You can use a single string if all the ids have the same code. If the ids are not filled in, then the datasets are combined without a join.
 * @param {boolean} [options.merge = false] - Use `true` to merge fields with the same name
 * @param {boolean} [options.all = true] - Use `true` to keep all elements.
 * @param {boolean} [options.emptygeom = true] - Use `false` to keep only data with geometries (if one ore more of your input data is a geoJSON).
 * @param {boolean} [options.fillkeys = true] - Use `true` to ensure that all features have all properties.
 * @returns {object|array} -  A GeoJSON FeatureCollection or an array of objects. (it depends on what you've set as `data`).
 * @example
 * geotoolbox.join([*a geojson*, *a dataset*], {ids:["ISO3", "id"], all: false)
 */
export function join(
  data,
  { ids, merge = false, emptygeom = true, all = true, fillkeys = true } = {}
) {
  // --------------
  // Data Handling
  // --------------

  // If !ids => conbine()
  if (ids == undefined) {
    return combine(data);
  } else {
    // Else process join

    // Deepcopy & geojson to flat array
    data = data.map((d) =>
      isgeojson(d)
        ? structuredClone(
            d.features.map((d) => ({
              ...d?.properties,
              ["#geometry#"]: d.geometry,
            }))
          )
        : structuredClone(d)
    );

    // ids

    if (typeof ids == "string") {
      ids = Array(data.length).fill(ids);
    }

    // Unique Fields

    if (!merge) {
      let keys = [];
      data.forEach((d) => {
        keys.push([...new Set(d.map((d) => Object.keys(d)).flat())]);
      });

      let uniquekeys = uniqueIdentifiersNested(keys);

      let newoldids = ids.map((d, i) => [
        d,
        uniquekeys[i][keys[i].indexOf(ids[i])],
      ]);

      ids = newoldids.map((d) => d[1]);
      data = renameKeysInArrays(data, uniquekeys);
    }
    // --------
    // Join All
    // --------

    // Join
    let output = data[0];
    for (let i = 1; i < data.length; i++) {
      output = mergeArrays(output, data[i], ids[0], ids[i], all);
    }

    // Fill
    if (fillkeys) {
      let prop = [...new Set(output.map((d) => Object.keys(d)).flat())];
      output = output.map((obj) =>
        Object.fromEntries(prop.map((key) => [key, obj[key]]))
      );
    }

    // ----------------------------------------
    // Rebuild a dataset (if #geometry# field)
    // ----------------------------------------

    if (
      [...new Set(output.map((d) => Object.keys(d)).flat())].includes(
        "#geometry#"
      )
    ) {
      if (!emptygeom) {
        output = output.filter((d) => d["#geometry#"] !== undefined);
      }

      const newprop = removeKeys(output, "#geometry#");
      let features = output.map((d, i) => ({
        type: "Feature",
        properties: newprop[i],
        geometry: d["#geometry#"],
      }));
      output = { type: "FeatureCollection", features };
    }

    return output;
  }
}

// --------------------------
// HELPERS
// ---------------------------

function uniqueIdentifiersNested(arr) {
  const count = new Map();
  return arr.map((subArr) =>
    subArr.map((item) => {
      if (item === "#geometry#") return item;
      const prefix = "_".repeat(count.get(item) || 0);
      count.set(item, (count.get(item) || 0) + 1);
      return prefix + item;
    })
  );
}

function renameKeysInArrays(arraysOfObjects, keysToReplace) {
  return arraysOfObjects.map((array, index) => {
    return array.map((obj) => {
      let newObj = {};
      let keyMap = keysToReplace[index];

      Object.keys(obj).forEach((key, i) => {
        let newKey = keyMap[i] || key; // Remplace la clé si une correspondance existe
        newObj[newKey] = obj[key];
      });

      return newObj;
    });
  });
}

function mergeArrays(arr1, arr2, key1, key2, all) {
  // Marge arr1 et arr2 (left join)
  const result = arr1.map((obj1) => {
    const matchedObj = arr2.find((obj2) => {
      const val1 = obj1[key1];
      const val2 = obj2[key2];
      return !isEmpty(val1) && !isEmpty(val2) && val1 === val2;
    });
    return matchedObj ? { ...obj1, ...matchedObj } : { ...obj1 };
  });

  // Add arr2 objects if they are not in arr1
  if (all) {
    arr2.forEach((obj2) => {
      const val2 = obj2[key2];
      if (
        !arr1.some((obj1) => {
          const val1 = obj1[key1];
          return !isEmpty(val1) && !isEmpty(val2) && val1 === val2;
        })
      ) {
        result.push({ ...obj2 });
      }
    });
  }

  return result;
}

function removeKeys(objects, keysToRemove) {
  return objects.map((obj) =>
    Object.fromEntries(
      Object.entries(obj).filter(([key]) => !keysToRemove.includes(key))
    )
  );
}

function isEmpty(value) {
  return value === undefined || value === null || value === "";
}