import { useEffect, useLayoutEffect, useRef } from 'preact/hooks'
import getSizeClass from '../shared/get-size-class.js'
import h from '../shared/h.js'

const MINIMUM_SUPPORTED_WIDTH = 48
const MAXIMUM_SUPPORTED_WIDTH = 3840
const DEBOUNCE = 100
const MAXIMUM_RESCALE_FACTOR = 1.3

function * generateWidths (factor) {
  for (let size = MINIMUM_SUPPORTED_WIDTH; size <= MAXIMUM_SUPPORTED_WIDTH; size *= factor) {
    yield Math.round(size)
  }
}

// Maximum linear upscale is 130%, only generate widths that produce scaling factor of 1.3 or less
const WIDTHS = [...generateWidths(MAXIMUM_RESCALE_FACTOR)].sort((a, b) => b - a)

// Global resize handler
const registeredImages = new Set()

let resizeId = null
// The resize handler is debounced to avoid excessive processing during window resizing.
const handleResize = () => {
  clearTimeout(resizeId)
  resizeId = setTimeout(() => {
    for (const img of registeredImages) {
      // Prune any images that may no longer be in the DOM
      if (!img.isConnected) {
        registeredImages.delete(img)
        continue
      }

      // Refresh image size parameters
      refreshImage(img)
    }
  }, DEBOUNCE)
}

// Register the resize handler
globalThis.addEventListener('resize', handleResize)

export function Img ({ _model, _fit = 'cover', _tall, role, ...props }) {
  const { src, alt = '', width, height } = _model
  if (!src) {
    return null
  }

  const sizeClass = getSizeClass(_tall)
  const ref = useRef(null)

  // Set the resize handler once for the current image on mount.
  useEffect(() => {
    const img = ref.current
    if (!img) {
      // For consistency, explicitly return undefined here.
      return undefined
    }

    registeredImages.add(img)
    return () => {
      registeredImages.delete(img)
    }
  }, [])

  // Set eager loading when the image is in the viewport on mount.
  useLayoutEffect(() => {
    const img = ref.current
    if (!img) {
      return
    }

    // Ensure the image size parameters are correctly set.
    refreshImage(img)

    // Check if any part of the image is part of the initial viewport (even partially).
    const rect = img.getBoundingClientRect()
    const viewportHeight = window.innerHeight || document.documentElement.clientHeight
    const viewportWidth = window.innerWidth || document.documentElement.clientWidth
    const isVisible = rect.bottom >= 0 && rect.right >= 0 && rect.top <= viewportHeight && rect.left <= viewportWidth

    // Ensure that the image is loaded eagerly if it is in the initial viewport. All those images are part of the Largest Contentful Paint.
    if (isVisible) {
      img.loading = 'eager'
      img.fetchPriority = 'high'
    }
  }, []) // Run once only on mount

  return h('img', {
    ...props,
    className: `cq-dd-image block object-${_fit} ${sizeClass}`,
    ref,
    src,
    loading: 'lazy',
    alt: role === 'presentation' ? '' : alt,
    role: alt ? role : 'presentation',
    width,
    height
  })
}

function refreshImage (img) {
  const src = img.getAttribute('src')
  const baseSrc = URL.parse(src, globalThis.location.href)
  if (!baseSrc) {
    return // Invalid URL
  }

  if (baseSrc.protocol === 'data:') {
    return // Do not refresh data URLs
  }

  const params = baseSrc.searchParams

  // Set the current pixel density of the device
  params.set('imdensity', window.devicePixelRatio || 1)

  // Set new width if larger than the current.
  const currentWidth = Number.parseInt(params.get('imwidth') || '0', 10)
  const newWidth = WIDTHS.find(w => w <= img.width) || WIDTHS.at(-1)
  if (newWidth > currentWidth) {
    params.set('imwidth', newWidth)
  }

  // Set image policy.
  const ratio = img.dataset.ratio ? `-${img.dataset.ratio}` : ''
  params.set('impolicy', `amidala-image${ratio}`)

  // Update image source.
  img.src = baseSrc.toString()
}
