/**
 * @file
 * Electoral map visualisation: solid fills for single-winner areas,
 * diagonal hatching for multi-winner areas.
 */

/* global L */

(function lgdElectoralMap($, Drupal) {
  const SVG_NS = 'http://www.w3.org/2000/svg';

  /**
   * Default style applied to every overlay boundary layer.
   */
  const boundaryStyle = {
    color: '#000000',
    weight: 2,
    opacity: 0.4,
    fillOpacity: 0.7,
    interactive: false,
    stroke: true,
  };

  // The initial zoom level, captured when the map is first processed.
  // Used as the baseline for scaling hatch widths.
  let initialZoom = null;

  /**
   * Find the SVG path element within a Leaflet layer.
   *
   * @param {object} boundaryLayer - The Leaflet layer.
   * @return {Element|null} The SVG path element, or null.
   */
  function findPathElement(boundaryLayer) {
    let pathElement = null;

    if (boundaryLayer.eachLayer) {
      boundaryLayer.eachLayer((subLayer) => {
        if (subLayer._path && !pathElement) {
          pathElement = subLayer._path;
        }
      });
    }

    return pathElement;
  }

  /**
   * Calculate hatch width scaled by zoom level relative to the initial view.
   *
   * @param {number} zoom - The current map zoom level.
   * @return {number} The scaled base width in pixels.
   */
  function getHatchWidth(zoom) {
    const baseWidth = 6;
    return baseWidth * 2 ** (zoom - initialZoom);
  }

  /**
   * Build or update a pattern element's coloured rects for a given hatch width.
   *
   * @param {Element} pattern - The SVG pattern element.
   * @param {Array} parties - Array of {color, seats} objects.
   * @param {number} hatchWidth - The base hatch width per seat in pixels.
   */
  function updatePatternSize(pattern, parties, hatchWidth) {
    while (pattern.firstChild) {
      pattern.removeChild(pattern.firstChild);
    }

    let x = 0;
    parties.forEach((p) => {
      const w = hatchWidth * p.seats;
      const rect = document.createElementNS(SVG_NS, 'rect');
      rect.setAttribute('x', x);
      rect.setAttribute('y', 0);
      rect.setAttribute('width', w);
      rect.setAttribute('height', 1);
      rect.setAttribute('fill', p.color);
      pattern.appendChild(rect);
      x += w;
    });

    pattern.setAttribute('width', x);
  }

  /**
   * Apply a diagonal hatch pattern to a boundary layer's SVG path.
   *
   * Creates an SVG pattern with diagonal stripes (315 degrees) where each
   * party's stripe width is proportional to the number of seats won.
   * The pattern rescales automatically on zoom.
   *
   * @param {object} boundaryLayer - The Leaflet layer to fill.
   * @param {Array} parties - Array of {color, seats} objects.
   * @param {string} patternId - Unique ID for the SVG pattern.
   * @param {object} map - The Leaflet map instance.
   */
  function applyHatchPattern(boundaryLayer, parties, patternId, map) {
    setTimeout(() => {
      const pathElement = findPathElement(boundaryLayer);
      if (!pathElement) {
        return;
      }

      const svg = pathElement.closest('svg');
      if (!svg) {
        return;
      }

      let defs = svg.querySelector('defs');
      if (!defs) {
        defs = document.createElementNS(SVG_NS, 'defs');
        svg.insertBefore(defs, svg.firstChild);
      }

      const pattern = document.createElementNS(SVG_NS, 'pattern');
      pattern.setAttribute('id', patternId);
      pattern.setAttribute('height', 1);
      pattern.setAttribute('patternUnits', 'userSpaceOnUse');
      pattern.setAttribute('patternTransform', 'rotate(-45)');

      updatePatternSize(pattern, parties, getHatchWidth(map.getZoom()));

      defs.appendChild(pattern);
      pathElement.setAttribute('fill', `url(#${patternId})`);
      pathElement.setAttribute('fill-opacity', '0.7');

      // Rescale hatching on zoom.
      map.on('zoomend', () => {
        updatePatternSize(pattern, parties, getHatchWidth(map.getZoom()));
      });
    }, 20);
  }

  /**
   * Bring boundary layers to the front so outlines sit above filled overlays.
   *
   * @param {object} map - The Leaflet map.
   */
  function bringBoundariesToFront(map) {
    map.eachLayer((layer) => {
      if (
        layer.setStyle &&
        layer.options &&
        layer.options.weight > 0 &&
        layer.options.fillOpacity === 0
      ) {
        if (layer.bringToFront) {
          layer.bringToFront();
        }
      }
    });
  }

  /**
   * Create overlay for a single-winner area with a solid party colour fill.
   *
   * @param {object} map - The Leaflet map.
   * @param {object} feature - The GeoJSON feature.
   * @param {string} partyColour - The party colour.
   */
  function createSingleWinnerOverlay(map, feature, partyColour) {
    L.geoJSON(feature, {
      style: { ...boundaryStyle, fillColor: partyColour },
    }).addTo(map);
  }

  /**
   * Create a hatched overlay for a multi-winner area.
   *
   * Consolidates winners by colour, then applies a diagonal hatch pattern
   * where each party's stripe width reflects seats won.
   *
   * @param {object} map - The Leaflet map.
   * @param {object} data - The area data with winners and boundary.
   * @param {string} nodeId - The node ID, used to create a unique pattern ID.
   */
  function createHatchOverlay(map, data, nodeId) {
    try {
      const boundary = JSON.parse(data.boundary);

      // Consolidate winners by colour.
      const parties = [];
      const seen = {};
      data.winners.forEach((winner) => {
        if (seen[winner.color]) {
          seen[winner.color].seats += 1;
        } else {
          const party = { color: winner.color, seats: 1 };
          seen[winner.color] = party;
          parties.push(party);
        }
      });

      const boundaryLayer = L.geoJSON(boundary, {
        style: { ...boundaryStyle, fillColor: '#000000' },
      });

      boundaryLayer.on('add', () => {
        applyHatchPattern(boundaryLayer, parties, `hatch-${nodeId}`, map);
      });

      boundaryLayer.addTo(map);
    } catch (error) {
      // eslint-disable-next-line no-console
      console.error('Error creating hatch overlay:', error);
    }
  }

  /**
   * Process electoral map data and apply visualisations.
   *
   * @param {object} mapData - The electoral map data keyed by node ID.
   */
  function processElectoralMapData(mapData) {
    const mapContainer = document.getElementById(
      'leaflet-map-view-localgov-election-electoral-map-page-map',
    );
    if (!mapContainer) {
      return;
    }

    const $mapContainer = $(mapContainer);
    const leafletData = $mapContainer.data('leaflet');

    if (!leafletData || !leafletData.lMap) {
      return;
    }

    const map = leafletData.lMap;

    // Prevent multiple processing.
    if (map._electoralMapProcessed) {
      return;
    }
    map._electoralMapProcessed = true;
    initialZoom = map.getZoom();

    // Process each electoral area.
    Object.keys(mapData).forEach((nodeId) => {
      const data = mapData[nodeId];

      if (data.winners.length === 1) {
        createSingleWinnerOverlay(
          map,
          JSON.parse(data.boundary),
          data.winners[0].color,
        );
      } else {
        createHatchOverlay(map, data, nodeId);
      }
    });

    // Ensure boundary layers appear on top of overlays.
    setTimeout(() => {
      bringBoundariesToFront(map);
    }, 20);
  }

  Drupal.behaviors.electoralMapBoundaries = {
    attach(context, settings) {
      if (!settings.electoralMapData) {
        return;
      }

      // Process immediately if map exists, otherwise wait briefly.
      if (
        typeof Drupal.leaflet !== 'undefined' &&
        Object.keys(Drupal.leaflet).length > 0
      ) {
        processElectoralMapData(settings.electoralMapData);
      } else {
        setTimeout(() => {
          processElectoralMapData(settings.electoralMapData);
        }, 50);
      }
    },
  };
})(jQuery, Drupal);
