import { Cloudinary, Configuration, Transformation } from 'cloudinary-core'

export type ElementOrString = HTMLElement | string
export type Size = { width: number; height: number }
export type BindType = ElementOrString | true
export type BindObject = BindType | { width?: BindType; height?: BindType }

export type InitParameters = { debug?: boolean }
export type ImageParameters = {
  src: string
  options?: Transformation | Transformation.Options
  bind?: BindObject
  lazy?: boolean | IntersectionObserverInit
  step?: number
}

let cl: Cloudinary | null = null
let DEBUG: boolean = false

function log(...msg: any[]) {
  if (DEBUG) console.debug(...msg)
}

export function initialize(cloudinary: Configuration.Options, options: InitParameters = {}) {
  DEBUG = options.debug || false
  cl = Cloudinary.new(cloudinary)
}

const defaults: Transformation | Transformation.Options = {
  fetchFormat: 'auto',
  quality: 'auto',
}

function calculateApproxRealSize(size: string, step: number): number {
  const withRatio = (parseInt(size) * window.devicePixelRatio) | 0
  const approx = withRatio - (withRatio % step) + step
  log(`Size\t withRation=${withRatio} approx=${approx} step=${step}`)
  return approx
}

function getSizeOfElement(el: HTMLElement, step: number): Size {
  const styles = window.getComputedStyle(el)
  log('GetSizeOfElement', el, `width=${styles.width} height=${styles.height}`)
  return {
    width: calculateApproxRealSize(styles.width, step),
    height: calculateApproxRealSize(styles.height, step),
  }
}

function getSizeOfElementOrSelector(node: ElementOrString, elOrString: ElementOrString, step: number): Size {
  if (typeof elOrString === 'string') {
    const search = typeof node === 'string' ? window.document.querySelector(node) : node
    if (!search) throw new Error('Could not find element: ' + node)
    const closest = search.closest<HTMLElement>(elOrString)
    if (closest) return getSizeOfElement(closest, step)
    else throw new Error('Could not find element: ' + elOrString)
  } else {
    return getSizeOfElement(elOrString, step)
  }
}

export function image(node: HTMLImageElement, parameters: ImageParameters) {
  if (!parameters || !parameters.src) throw new Error('No url provided for cloudinary')

  let { src, options, bind, lazy, step } = parameters
  log('Image Declared', parameters)
  options = options ?? {}
  step = step ?? 200
  lazy = lazy ?? true

  if (!cl) throw new Error('Cloudinary not initialized')
  if (!src) throw new Error('Src must be set in use:image')

  if (bind) {
    if (bind === true) {
      bind = node
    }
    if (!options.crop) options.crop = 'fill'

    if (bind instanceof Element) Object.assign(options, getSizeOfElement(bind, step))
    else if (typeof bind === 'string') {
      Object.assign(options, getSizeOfElementOrSelector(node, bind, step))
    } else if (typeof bind === 'object') {
      if (bind.width) {
        options.width = getSizeOfElementOrSelector(node, bind.width === true ? node : bind.width, step).width
      }
      if (bind.height) {
        options.height = getSizeOfElementOrSelector(node, bind.height === true ? node : bind.height, step).height
      }
    }
  }

  const all: Transformation | Transformation.Options = { ...defaults, ...options }
  const attrs: any = cl.imageTag(parameters.src, all).attributes()
  log('Attributes', attrs)
  const replace = () => (node.src = attrs.src)

  if (lazy && typeof IntersectionObserver !== 'undefined') {
    const options: IntersectionObserverInit = lazy === true ? { rootMargin: '25%', threshold: 0 } : lazy
    new IntersectionObserver((entries, observer) => {
      if (entries[0].isIntersecting) {
        observer.disconnect()
        replace()
      }
    }, options).observe(node)
  } else {
    replace()
  }
}