<?php

namespace Drupal\simple_interactive_maps;

use function abs;
use function count;
use function max;
use function min;
use function preg_split;
use function sqrt;
use function substr;

/**
 * Helper class for working with SVG paths.
 */
final class SvgHelper {

  /**
   * Get the bounding box of an SVG path.
   *
   * @param string $path
   *   The SVG path.
   *
   * @return array<float>
   *   An array containing the x, y, width, and height of the bounding box.
   */
  public static function getBoundingBox(string $path): array {
    $commands = preg_split('/(?=[MmZzLlHhVvCcSsQqTtAa])/', $path, -1, PREG_SPLIT_NO_EMPTY);

    $minX = INF;
    $maxX = -INF;
    $minY = INF;
    $maxY = -INF;

    $currentX = 0;
    $currentY = 0;

    foreach ($commands as $command) {
      $type = $command[0];
      $args = \preg_split('/[\s,]+/', substr($command, 1));

      switch ($type) {
        case 'M':
        case 'L':
          for ($i = 0; $i < count($args); $i += 2) {
            $currentX = $args[$i];
            $currentY = $args[$i + 1];
            $minX = min($minX, $currentX);
            $maxX = max($maxX, $currentX);
            $minY = \min($minY, $currentY);
            $maxY = \max($maxY, $currentY);
          }

          break;

        case 'H':
          foreach ($args as $x) {
            $currentX = $x;
            $minX = \min($minX, $currentX);
            $maxX = \max($maxX, $currentX);
          }

          break;

        case 'V':
          foreach ($args as $y) {
            $currentY = $y;
            $minY = \min($minY, $currentY);
            $maxY = \max($maxY, $currentY);
          }

          break;

        case 'C':
          for ($i = 0; $i < \count($args); $i += 6) {
            $x1 = $args[$i];
            $y1 = $args[$i + 1];
            $x2 = $args[$i + 2];
            $y2 = $args[$i + 3];
            $x = $args[$i + 4];
            $y = $args[$i + 5];

            $controlPoints = self::calculateCubicBezierBoundingBox(
                (float) $currentX,
                (float) $currentY,
                (float) $x1,
                (float) $y1,
                (float) $x2,
                (float) $y2,
                (float) $x,
                (float) $y,
            );

            $minX = \min($minX, ...$controlPoints['x']);
            $maxX = \max($maxX, ...$controlPoints['x']);
            $minY = \min($minY, ...$controlPoints['y']);
            $maxY = \max($maxY, ...$controlPoints['y']);

            $currentX = $x;
            $currentY = $y;
          }

          break;
      }
    }

    return [$minX, $minY, $maxX - $minX, $maxY - $minY];
  }

  /**
   * Calculate the viewBox attribute for an SVG element containing paths.
   *
   * @param array<string> $paths
   *   An array of SVG paths.
   *
   * @return string
   *   The viewBox attribute value.
   */
  public static function calculateViewBox(array $paths): string {
    $minX = INF;
    $maxX = -INF;
    $minY = INF;
    $maxY = -INF;

    foreach ($paths as $path) {
      [$x, $y, $width, $height] = self::getBoundingBox($path);
      $minX = \min($minX, $x);
      $maxX = \max($maxX, $x + $width);
      $minY = \min($minY, $y);
      $maxY = \max($maxY, $y + $height);
    }

    return "$minX $minY " . ($maxX - $minX) . " " . ($maxY - $minY);
  }

  /**
   * Calculate the bounding box of a cubic Bezier curve.
   *
   * Thanks to ChatGPT for the code snippet. This is math I've not
   * looked at in 30 years or more.
   *
   * @param float $x0
   *   The x-coordinate of the starting point.
   * @param float $y0
   *   The y-coordinate of the starting point.
   * @param float $x1
   *   The x-coordinate of the first control point.
   * @param float $y1
   *   The y-coordinate of the first control point.
   * @param float $x2
   *   The x-coordinate of the second control point.
   * @param float $y2
   *   The y-coordinate of the second control point.
   * @param float $x
   *   The x-coordinate of the ending point.
   * @param float $y
   *   The y-coordinate of the ending point.
   *
   * @return array<mixed>
   *   An array containing the x and y coordinates of the bounding box.
   */
  private static function calculateCubicBezierBoundingBox(
    float $x0,
    float $y0,
    float $x1,
    float $y1,
    float $x2,
    float $y2,
    float $x,
    float $y,
  ): array {
    // Function to calculate the bounding box of a cubic Bezier curve.
    $tValues = [];
    $xValues = [];
    $yValues = [];

    for ($i = 0; $i <= 2; $i++) {
      if ($i === 0) {
        $b = 6 * $x0 - 12 * $x1 + 6 * $x2;
        $a = -3 * $x0 + 9 * $x1 - 9 * $x2 + 3 * $x;
        $c = 3 * $x1 - 3 * $x0;
      }
      else {
        $b = 6 * $y0 - 12 * $y1 + 6 * $y2;
        $a = -3 * $y0 + 9 * $y1 - 9 * $y2 + 3 * $y;
        $c = 3 * $y1 - 3 * $y0;
      }

      if (abs($a) < 1e-12) {
        if (\abs($b) < 1e-12) {
          continue;
        }

        $t = -$c / $b;

        if ($t > 0 && $t < 1) {
          $tValues[] = $t;
        }

        continue;
      }

      $b2ac = $b * $b - 4 * $c * $a;

      if ($b2ac < 0) {
        continue;
      }

      $sqrtb2ac = sqrt($b2ac);
      $t1 = (-$b + $sqrtb2ac) / (2 * $a);
      $t2 = (-$b - $sqrtb2ac) / (2 * $a);

      if ($t1 > 0 && $t1 < 1) {
        $tValues[] = $t1;
      }

      if ($t2 <= 0 || $t2 >= 1) {
        continue;
      }

      $tValues[] = $t2;
    }

    foreach ($tValues as $t) {
      $mt = 1 - $t;
      $xValues[] = ($mt * $mt * $mt * $x0) + (3 * $mt * $mt * $t * $x1) + (3 * $mt * $t * $t * $x2) + ($t * $t * $t * $x);
      $yValues[] = ($mt * $mt * $mt * $y0) + (3 * $mt * $mt * $t * $y1) + (3 * $mt * $t * $t * $y2) + ($t * $t * $t * $y);
    }

    $xValues[] = $x0;
    $xValues[] = $x;
    $yValues[] = $y0;
    $yValues[] = $y;

    return ['x' => $xValues, 'y' => $yValues];
  }

}
