/**
 * @file map-utils.js
 * @summary Map utilities for MapLibre maps.
 *
 * @description
 * Utilities to assist with general map management, compatible with MapLibre GL JS.
 */

/**
 * Add any images (for pin icons) to the map source.
 *
 * @param {object} map A MapLibre map instance.
 * @param {object[]} imageSources Array of image source objects with `id` and `url` properties.
 * @return {void}
 */
function addImagesToMap(map, imageSources) {
  if (imageSources) {
    // Add any image sources to the map
    imageSources.forEach(function (imageSource) {
      _addImageToMapSource(map, imageSource.id, imageSource.url);
    });
  }
}

/**
 * Add an image (for pin icons) to the map source.
 *
 * @param {object} map A MapLibre map instance.
 * @param {string} imageId ID of the image to add.
 * @param {string} imageUrl The local path to the image to add to the map.
 * @returns {void}
 */
async function _addImageToMapSource(map, imageId, imageUrl) {
  if (!map || !imageId || !imageUrl) return;
  const response = await map.loadImage(imageUrl);
  // Add the loaded image to the style's sprite with the ID 'photo'.
  map.addImage(imageId, response.data);
}

/**
 * Get the bounds of point features in a MapLibre map.
 *
 * @param {object[]} features MapLibre features array.
 * @returns {int[]} Bounds of the point features.
 */
function getPointFeaturesBounds(features) {
  if (!features.length) return null;
  // Initialize bounds with the first point
  let bounds = new maplibregl.LngLatBounds(
    features[0].geometry.coordinates,
    features[0].geometry.coordinates
  );
  for (let i = 1; i < features.length; i++) {
    bounds.extend(features[i].geometry.coordinates);
  }
  return bounds;
}

/**
 * Add a FeatureServer layer to the map instance.
 *
 * @note `url` should end with `/FeatureServer`,
 *        i.e., `"https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/World_Latitude_and_Longitude_Grids/FeatureServer"`.
 *
 * @param {object} el Widget element containing the map instance.
 * @param {string} url URL of the ArcGIS FeatureServer layer to add.
 * @param {string} sourceId ID of the source to add.
 * @returns {void}
 */
function addFeatureServerSource(el, url, sourceId) {
  url = url + '/0/query?where=1=1&outFields=*&f=geojson';
  el.mapInstance.addSource(sourceId, {
    type: 'geojson',
    data: url,
  });
}

/**
 * Close the attribution control on a MapLibre map once it has loaded.
 *
 * @param {string} mapId ID of the map element.
 * @param {object} widgetInstance Map widget instance containing the map.
 * @returns {void}
 */
function closeAttribution(mapId, widgetInstance) {
  waitForFullLoad(widgetInstance, () => {
    let map = document.getElementById(mapId);
    const attributionControl = map.querySelector('.maplibregl-ctrl-attrib-button');
    if (attributionControl) {
      attributionControl.click();
    }
  });
}

/**
 * Wait for the MapLibre map to fully load before executing a callback function.
 *
 * @param {object} widgetInstance Map widget instance containing the map.
 * @param {function} callback Function to execute once the map has fully loaded.
 * @return {void}
 */
function waitForFullLoad(widgetInstance, callback) {
  const map = widgetInstance.getMap();
  if (map.loaded()) {
    callback();
  } else {
    map.once('idle', callback);
  }
}

/**
 * Add a loading overlay to the map element until the map loads initially.
 *
 * @param {object} el Widget element containing the map instance.
 * @param {boolean} [changeLoader=false] Whether to use the initial or busy loader style.
 * @param {string} [bgColour="white"] Background colour of the loading overlay.
 * @param {string} [loaderColour="black"] Colour of the loader.
 * @returns {void}
 */
function addMapLoader(el, changeLoader = false, bgColour = 'white', loaderColour = 'black') {
  // Add a loading overlay div
  const loadingDiv = document.createElement('div');
  loadingDiv.className =
    'toro-loading-overlay' + (changeLoader ? ' busy-loader' : ' initial-loader');
  loadingDiv.style.setProperty('--loader-bg-colour', bgColour);
  loadingDiv.innerHTML = '<div class="loader" style="--loader-colour:' + loaderColour + ';"></div>';
  el.appendChild(loadingDiv);
}

/**
 * Add a loading overlay to the map element until the map loads initially.
 *
 * @param {object} el Widget element containing the map instance.
 * @returns {void}
 */
function removeMapLoader(el) {
  // Add a loading overlay div
  const loadingDiv = el.querySelector('.toro-loading-overlay');
  if (!loadingDiv) return;
  loadingDiv.remove();
}

/**
 * Convert a hex colour string to RGB values.
 *
 * @param {string} hex Hex colour string (e.g., "#000000").
 * @returns {string} RGB values as a string (e.g., "0,0,0").
 */
function hexToRgbValues(hex) {
  let c = hex.replace('#', '');
  if (c.length === 3)
    c = c
      .split('')
      .map((x) => x + x)
      .join('');
  const num = parseInt(c, 16);
  const r = (num >> 16) & 255;
  const g = (num >> 8) & 255;
  const b = num & 255;
  return `${r},${g},${b}`;
}

/**
 * Convert a rgb colour string to RGB values.
 *
 * @param {string} rgb RGB colour string (e.g., "rgb(0,0,0)").
 * @returns {string} RGB values as a string (e.g., "0,0,0").
 */
function rgbToRgbValues(rgb) {
  return rgb.replace(/^rgb\((.*)\)$/i, '$1');
}

/**
 * Convert a css colour name string to RGB values.
 *
 * @param {string} name Colour string name (e.g., "black").
 * @returns {string} RGB values as a string (e.g., "0,0,0").
 */
function nameToRgbValues(name) {
  const ctx = document.createElement('canvas').getContext('2d');
  ctx.fillStyle = name;
  const hex = ctx.fillStyle;
  return hexToRgbValues(hex);
}

/**
 * Convert a css colour string to RGB values.
 * Can handle hex, rgb, rgba, and named colours.
 *
 * @param {string} colour  Colour string (e.g., "#000000", "rgb(0,0,0)", "black").
 * @returns {string} RGB values as a string (e.g., "0,0,0").
 */
function toRgbValues(colour) {
  if (colour.startsWith('#')) {
    return hexToRgbValues(colour);
  } else if (colour.startsWith('rgb(')) {
    return rgbToRgbValues(colour);
  } else if (colour.startsWith('rgba(')) {
    // Optionally replace the alpha value
    return colour.replace(/rgba\(([^,]+),([^,]+),([^,]+),([^)]+)\)/, `$1,$2,$3`);
  } else {
    return nameToRgbValues(colour);
  }
}

/**
 * Get tile IDs from a tiles item, which can be an array, string, or object.
 *
 * @param {array|string|object} tilesItem The tiles item to extract tile IDs from.
 * @returns {array} Array of tile IDs.
 */
function getTileIds(tilesItem) {
  if (!tilesItem) return [];
  if (Array.isArray(tilesItem)) return tilesItem;
  if (typeof tilesItem === 'string') return [tilesItem];
  if (typeof tilesItem === 'object' && tilesItem !== null) {
    return Object.keys(tilesItem);
  }
  return [];
}

/**
 * Update Shiny with the clicked feature on the map.
 * The ID it updates is the map ID with "_feature_click" appended to it (i.e., `<mapId>_feature_click`).
 *
 * @param {string} mapId ID of the map to set the Shiny feature click for
 * @param {string} layerId ID of the layer that was clicked
 * @param {object} feature The clicked feature object
 * @returns {void}
 */
function setShinyClickedFeature(mapId, layerId, feature) {
  if (!mapId || !layerId || !feature) return;
  // Trigger a Shiny input event with the clicked feature's properties
  Shiny.setInputValue(`${mapId}_feature_click`, {
    layerId: layerId,
    properties: feature.properties,
    geometry: feature.geometry,
    time: new Date().toISOString(), // For multiple clicks on the same feature
  });
}
