<?php

declare(strict_types=1);

namespace Drupal\media_image_metadata\Service;

use Drupal\Component\Utility\Html;
use Drupal\Core\Lock\NullLockBackend;

/**
 * Helper service for extracting image metadata (EXIF, IPTC, XMP).
 */
class ImageMetadataHelper {

  /**
   * Get raw metadata from an image file.
   *
   * @param string $file_path
   *   Path to the image file.
   *
   * @return array
   *   Associative array with keys: 'exif', 'iptc', 'xmp'.
   *
   * @throws \RuntimeException
   *   When the file is not readable.
   */
  public function getMetadataRaw(string $file_path): array {
    $metadata = [
      'exif' => [],
      'iptc' => [],
      'xmp' => [],
    ];

    if (!is_readable($file_path)) {
      throw new \RuntimeException("File not readable: $file_path");
    }

    // EXIF Metadata.
    if (function_exists('exif_read_data')) {
      $exif = @exif_read_data($file_path, 'ANY_TAG', TRUE);
      if ($exif !== FALSE) {
        $metadata['exif'] = $exif;
      }
    }

    // IPTC Metadata.
    $info = [];
    $size = @getimagesize($file_path, $info);
    if (isset($info['APP13'])) {
      $iptc = iptcparse($info['APP13']);
      if ($iptc !== FALSE) {
        foreach ($iptc as $tag => $value) {
          $metadata['iptc'][$tag] = is_array($value) && count($value) === 1 ? $value[0] : $value;
        }
      }
    }

    // XMP Metadata.
    $metadata['xmp'] = $this->extractXmp($file_path);

    return $metadata;
  }

  /**
   * Extract XMP from the image file.
   *
   * @param string $file_path
   *   Path to the image file.
   *
   * @return array
   *   Key-value pairs from XMP data.
   */
  private function extractXmp(string $file_path): array {
    $result = [];
    $data = @file_get_contents($file_path);

    if ($data !== FALSE && preg_match('/<x:xmpmeta.*?<\/x:xmpmeta>/s', $data, $matches)) {
      $xmp = $matches[0];
      try {
        $dom = Html::load($xmp);
        $xpath = new \DOMXPath($dom);

        // Query all elements that don't have child elements (leaf nodes).
        $xpath_query = "//*[not(*)]";
        foreach ($xpath->query($xpath_query) as $element) {
          assert($element instanceof \DOMElement);

          $name = $element->nodeName;
          $value = trim($element->textContent);

          // Skip empty values.
          if ($value === '') {
            continue;
          }

          // Build a hierarchical name if the element has a parent with a
          // different name.
          $parent = $element->parentNode;
          if ($parent instanceof \DOMElement && $parent->nodeName !== $name) {
            $name = $parent->nodeName . ':' . $name;
          }

          $result[$name] = $value;
        }
      }
      catch (\Exception $e) {
        // If XML parsing fails, silently continue.
      }
    }

    return $result;
  }

  /**
   * Normalize metadata into a flat, human-friendly array.
   *
   * @param array $raw
   *   Raw metadata as returned by self::getMetadataRaw().
   *
   * @return array
   *   Normalized metadata.
   */
  public function normalizeMetadata(array $raw): array {
    $iptc = $raw['iptc'];
    $xmp = $raw['xmp'];
    $exif = $raw['exif'];

    return [
      'title' => $this->firstNonEmpty([
        $iptc['2#005'] ?? NULL,
        $xmp['dc:title'] ?? NULL,
        $iptc['2#105'] ?? NULL,
        $exif['IFD0']['ImageDescription'] ?? NULL,
        $exif['FILE']['FileName'] ?? NULL,
      ]),
      'alt' => $this->firstNonEmpty([
        $xmp['rdf:alt:rdf:li'] ?? NULL,
        $xmp['rdf:description:photoshop:headline'] ?? NULL,
        $iptc['2#105'] ?? NULL,
        $iptc['2#120'] ?? NULL,
      ]),
      'caption' => $this->firstNonEmpty([
        $iptc['2#120'] ?? NULL,
        $xmp['dc:description'] ?? NULL,
        $exif['IFD0']['ImageDescription'] ?? NULL,
      ]),
      'credit' => $this->firstNonEmpty([
        $iptc['2#110'] ?? NULL,
        $exif['IFD0']['Artist'] ?? NULL,
        $xmp['photoshop:Credit'] ?? NULL,
      ]),
      'copyright' => $this->firstNonEmpty([
        $iptc['2#116'] ?? NULL,
        $exif['IFD0']['Copyright'] ?? NULL,
        $xmp['dc:rights'] ?? NULL,
      ]),
      'headline' => $this->firstNonEmpty([
        $iptc['2#105'] ?? NULL,
        $xmp['photoshop:Headline'] ?? NULL,
      ]),
      'date_created' => $this->firstNonEmpty([
        $iptc['2#055'] ?? NULL,
        $exif['IFD0']['DateTime'] ?? NULL,
        $xmp['photoshop:DateCreated'] ?? NULL,
        $exif['FILE']['FileDateTime'] ?? NULL,
      ]),
      'byline' => $iptc['2#080'] ?? NULL,
      'city' => $iptc['2#090'] ?? NULL,
      'province' => $iptc['2#095'] ?? NULL,
      'country' => $iptc['2#101'] ?? NULL,
      'source' => $iptc['2#115'] ?? NULL,
      'category' => $iptc['2#015'] ?? NULL,
      'supp_cat' => $iptc['2#020'] ?? NULL,
      // Additional EXIF camera data.
      'camera_model' => $this->firstNonEmpty([
        $exif['IFD0']['Model'] ?? NULL,
        $exif['EXIF']['Model'] ?? NULL
      ]),
      'iso' => $exif['EXIF']['ISOSpeedRatings'] ?? NULL,
      'exposure' => !empty($exif['EXIF']['ExposureTime']) ? $this->normalizeFraction($exif['EXIF']['ExposureTime']) : NULL,
      'aperture' => !empty($exif['EXIF']['FNumber']) ? $this->normalizeFraction($exif['EXIF']['FNumber']) : NULL,
      'focal_length' => !empty($exif['EXIF']['FocalLength']) ? $this->normalizeFraction($exif['EXIF']['FocalLength']) : NULL,
    ];
  }

  /**
   * Helper to return the first non-empty value of a series of values.
   *
   * @param array $values
   *   An indexed list of values to check.
   *
   * @return mixed|null
   *   The first non-empty value in the array, checked with !empty().
   */
  private function firstNonEmpty(array $values) {
    foreach ($values as $value) {
      if (!empty($value)) {
        return $value;
      }
    }
    return NULL;
  }

  /**
   * Normalize fractions.
   *
   * @param string $fraction
   *   The fraction string to normalize (e.g., "1/60", "28/10").
   *
   * @return string|int|float
   *   The normalized fraction value.
   */
  private function normalizeFraction(string $fraction) {
    $parts = explode('/', $fraction);
    $top = (int) $parts[0];
    $bottom = (int) $parts[1];

    if ($top > $bottom) {
      // Value > 1.
      if (($top % $bottom) == 0) {
        $value = ($top / $bottom);
      }
      else {
        $value = round(($top / $bottom), 2);
      }
    }
    else {
      if ($top == $bottom) {
        // Value = 1.
        $value = '1';
      }
      else {
        // Value < 1.
        if ($top == 1) {
          $value = '1/' . $bottom;
        }
        else {
          if ($top != 0) {
            $value = '1/' . round(($bottom / $top), 0);
          }
          else {
            $value = '0';
          }
        }
      }
    }
    return $value;
  }

}
