// Styles
import './VImg.sass'

// Directives
import intersect from '../../directives/intersect'

// Types
import { VNode } from 'vue'
import { PropValidator } from 'vue/types/options'

// Components
import VResponsive from '../VResponsive'

// Mixins
import Themeable from '../../mixins/themeable'

// Utils
import mixins from '../../util/mixins'
import mergeData from '../../util/mergeData'
import { consoleWarn } from '../../util/console'
import { getSlot } from '../../util/helpers'

// not intended for public use, this is passed in by vuetify-loader
export interface srcObject {
  src: string
  srcset?: string
  lazySrc: string
  aspect: number
}

const hasIntersect = typeof window !== 'undefined' && 'IntersectionObserver' in window

/* @vue/component */
export default mixins(
  VResponsive,
  Themeable,
).extend({
  name: 'v-img',

  directives: { intersect },

  props: {
    alt: String,
    contain: Boolean,
    eager: Boolean,
    gradient: String,
    lazySrc: String,
    options: {
      type: Object,
      // For more information on types, navigate to:
      // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
      default: () => ({
        root: undefined,
        rootMargin: undefined,
        threshold: undefined,
      }),
    } as PropValidator<IntersectionObserverInit>,
    position: {
      type: String,
      default: 'center center',
    },
    sizes: String,
    src: {
      type: [String, Object],
      default: '',
    } as PropValidator<string | srcObject>,
    srcset: String,
    transition: {
      type: [Boolean, String],
      default: 'fade-transition',
    },
  },

  data () {
    return {
      currentSrc: '', // Set from srcset
      image: null as HTMLImageElement | null,
      isLoading: true,
      calculatedAspectRatio: undefined as number | undefined,
      naturalWidth: undefined as number | undefined,
      hasError: false,
    }
  },

  computed: {
    computedAspectRatio (): number {
      return Number(this.normalisedSrc.aspect || this.calculatedAspectRatio)
    },
    normalisedSrc (): srcObject {
      return this.src && typeof this.src === 'object'
        ? {
          src: this.src.src,
          srcset: this.srcset || this.src.srcset,
          lazySrc: this.lazySrc || this.src.lazySrc,
          aspect: Number(this.aspectRatio || this.src.aspect),
        } : {
          src: this.src,
          srcset: this.srcset,
          lazySrc: this.lazySrc,
          aspect: Number(this.aspectRatio || 0),
        }
    },
    __cachedImage (): VNode | [] {
      if (!(this.normalisedSrc.src || this.normalisedSrc.lazySrc || this.gradient)) return []

      const backgroundImage: string[] = []
      const src = this.isLoading ? this.normalisedSrc.lazySrc : this.currentSrc

      if (this.gradient) backgroundImage.push(`linear-gradient(${this.gradient})`)
      if (src) backgroundImage.push(`url("${src}")`)

      const image = this.$createElement('div', {
        staticClass: 'v-image__image',
        class: {
          'v-image__image--preload': this.isLoading,
          'v-image__image--contain': this.contain,
          'v-image__image--cover': !this.contain,
        },
        style: {
          backgroundImage: backgroundImage.join(', '),
          backgroundPosition: this.position,
        },
        key: +this.isLoading,
      })

      /* istanbul ignore if */
      if (!this.transition) return image

      return this.$createElement('transition', {
        attrs: {
          name: this.transition,
          mode: 'in-out',
        },
      }, [image])
    },
  },

  watch: {
    src () {
      // Force re-init when src changes
      if (!this.isLoading) this.init(undefined, undefined, true)
      else this.loadImage()
    },
    '$vuetify.breakpoint.width': 'getSrc',
  },

  mounted () {
    this.init()
  },

  methods: {
    init (
      entries?: IntersectionObserverEntry[],
      observer?: IntersectionObserver,
      isIntersecting?: boolean
    ) {
      // If the current browser supports the intersection
      // observer api, the image is not observable, and
      // the eager prop isn't being used, do not load
      if (
        hasIntersect &&
        !isIntersecting &&
        !this.eager
      ) return

      if (this.normalisedSrc.lazySrc) {
        const lazyImg = new Image()
        lazyImg.src = this.normalisedSrc.lazySrc
        this.pollForSize(lazyImg, null)
      }
      /* istanbul ignore else */
      if (this.normalisedSrc.src) this.loadImage()
    },
    onLoad () {
      this.getSrc()
      this.isLoading = false
      this.$emit('load', this.src)

      if (
        this.image &&
        (this.normalisedSrc.src.endsWith('.svg') || this.normalisedSrc.src.startsWith('data:image/svg+xml'))
      ) {
        if (this.image.naturalHeight && this.image.naturalWidth) {
          this.naturalWidth = this.image.naturalWidth
          this.calculatedAspectRatio = this.image.naturalWidth / this.image.naturalHeight
        } else {
          this.calculatedAspectRatio = 1
        }
      }
    },
    onError () {
      this.hasError = true
      this.$emit('error', this.src)
    },
    getSrc () {
      /* istanbul ignore else */
      if (this.image) this.currentSrc = this.image.currentSrc || this.image.src
    },
    loadImage () {
      const image = new Image()
      this.image = image

      image.onload = () => {
        /* istanbul ignore if */
        if (image.decode) {
          image.decode().catch((err: DOMException) => {
            consoleWarn(
              `Failed to decode image, trying to render anyway\n\n` +
              `src: ${this.normalisedSrc.src}` +
              (err.message ? `\nOriginal error: ${err.message}` : ''),
              this
            )
          }).then(this.onLoad)
        } else {
          this.onLoad()
        }
      }
      image.onerror = this.onError

      this.hasError = false
      this.sizes && (image.sizes = this.sizes)
      this.normalisedSrc.srcset && (image.srcset = this.normalisedSrc.srcset)
      image.src = this.normalisedSrc.src
      this.$emit('loadstart', this.normalisedSrc.src)

      this.aspectRatio || this.pollForSize(image)
      this.getSrc()
    },
    pollForSize (img: HTMLImageElement, timeout: number | null = 100) {
      const poll = () => {
        const { naturalHeight, naturalWidth } = img

        if (naturalHeight || naturalWidth) {
          this.naturalWidth = naturalWidth
          this.calculatedAspectRatio = naturalWidth / naturalHeight
        } else if (!img.complete && this.isLoading && !this.hasError && timeout != null) {
          setTimeout(poll, timeout)
        }
      }

      poll()
    },
    genContent () {
      const content: VNode = VResponsive.options.methods.genContent.call(this)
      if (this.naturalWidth) {
        this._b(content.data!, 'div', {
          style: { width: `${this.naturalWidth}px` },
        })
      }

      return content
    },
    __genPlaceholder (): VNode | void {
      const slot = getSlot(this, 'placeholder')
      if (slot) {
        const placeholder = this.isLoading
          ? [this.$createElement('div', {
            staticClass: 'v-image__placeholder',
          }, slot)]
          : []

        if (!this.transition) return placeholder[0]

        return this.$createElement('transition', {
          props: {
            appear: true,
            name: this.transition,
          },
        }, placeholder)
      }
    },
  },

  render (h): VNode {
    const node = VResponsive.options.render.call(this, h)

    const data = mergeData(node.data!, {
      staticClass: 'v-image',
      attrs: {
        'aria-label': this.alt,
        role: this.alt ? 'img' : undefined,
      },
      class: this.themeClasses,
      // Only load intersect directive if it
      // will work in the current browser.
      directives: hasIntersect
        ? [{
          name: 'intersect',
          modifiers: { once: true },
          value: {
            handler: this.init,
            options: this.options,
          },
        }]
        : undefined,
    })

    node.children = [
      this.__cachedSizer,
      this.__cachedImage,
      this.__genPlaceholder(),
      this.genContent(),
    ] as VNode[]

    return h(node.tag, data, node.children)
  },
})
