import { Color, RGBAColor, HSVAColor } from './color'
import { isNumber, isString } from './types'
import { clamp, Position, getMousePosition } from './utils'
type ColorPickerOptions = {
  window?: Window
  el?: HTMLElement | string
  background?: string | number
  widthUnits?: string
  heightUnits?: string
  width?: number
  height?: number
  color?: string | number
}
export class ColorPicker {
  private _window: Window
  private _document: Document
  private _widthUnits: string = 'px'
  private _heightUnits: string = 'px'
  private _huePosition: number = 0
  private _hueHeight: number = 0
  private _maxHue: number = 0
  public _inputIsNumber: boolean = false
  private _saturationWidth: number = 0
  private _isChoosing: boolean = false
  private _callbacks: Function[] = []
  public width: number = 0
  public height: number = 0
  public hue: number = 0
  public position: Position = { x: 0, y: 0 }
  public color: Color = new Color(0)
  public backgroundColor: Color = new Color(0)
  public hueColor: Color = new Color(0)
  public $el: HTMLElement
  public $saturation: HTMLElement
  public $hue: HTMLElement
  public $sbSelector: HTMLElement
  public $hSelector: HTMLElement
  constructor(options: ColorPickerOptions = {}) {
    // Register window and document references in case this is instantiated inside of an iframe
    this._window = options.window || window
    this._document = this._window.document
    // Create DOM
    this.$el = this._document.createElement('div')
    this.$el.className = 'Scp'
    this.$el.innerHTML = `
      
      
    `
    // DOM accessors
    this.$saturation = this.$el.querySelector('.Scp-saturation')
    this.$hue = this.$el.querySelector('.Scp-hue')
    this.$sbSelector = this.$el.querySelector('.Scp-sbSelector')
    this.$hSelector = this.$el.querySelector('.Scp-hSelector')
    // Event listeners
    this.$saturation.addEventListener('mousedown', this._onSaturationMouseDown)
    this.$saturation.addEventListener('touchstart', this._onSaturationMouseDown)
    this.$hue.addEventListener('mousedown', this._onHueMouseDown)
    this.$hue.addEventListener('touchstart', this._onHueMouseDown)
    // Some styling and DOMing from options
    if (options.el) {
      this.appendTo(options.el)
    }
    if (options.background) {
      this.setBackgroundColor(options.background)
    }
    if (options.widthUnits) {
      this._widthUnits = options.widthUnits
    }
    if (options.heightUnits) {
      this._heightUnits = options.heightUnits
    }
    this.setSize(options.width || 175, options.height || 150)
    this.setColor(options.color)
  }
  /**
   * Add the ColorPicker instance to a DOM element.
   * @param  {HTMLElement} el
   * @return {ColorPicker} Returns itself for chaining purpose
   */
  public appendTo(el: HTMLElement | string): ColorPicker {
    if (isString(el)) {
      document.querySelector(el as string).appendChild(this.$el)
    } else {
      ;(el as HTMLElement).appendChild(this.$el)
    }
    return this
  }
  /**
   * Removes picker from its parent and kill all listeners.
   * Call this method for proper destroy.
   */
  public remove() {
    this._callbacks = []
    this._onSaturationMouseUp()
    this._onHueMouseUp()
    this.$saturation.removeEventListener(
      'mousedown',
      this._onSaturationMouseDown
    )
    this.$saturation.removeEventListener(
      'touchstart',
      this._onSaturationMouseDown
    )
    this.$hue.removeEventListener('mousedown', this._onHueMouseDown)
    this.$hue.removeEventListener('touchstart', this._onHueMouseDown)
    // this.off()
    if (this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el)
    }
  }
  /**
   * Manually set the current color of the picker. This is the method
   * used on instantiation to convert `color` option to actual color for
   * the picker. Param can be a hexadecimal number or an hex String.
   * @param {String|Number} color hex color desired
   * @return {ColorPicker} Returns itself for chaining purpose
   */
  public setColor(color: string | number): ColorPicker {
    this._inputIsNumber = isNumber(color)
    this.color.fromHex(color)
    const { h, s, v } = this.color.hsv
    if (!isNaN(h)) {
      this.hue = h
    }
    this._moveSelectorTo(this._saturationWidth * s, (1 - v) * this._hueHeight)
    this._moveHueTo((1 - this.hue) * this._hueHeight)
    this._updateHue()
    return this
  }
  /**
   * Set size of the color picker for a given width and height. Note that
   * a padding of 5px will be added if you chose to use the background option
   * of the constructor.
   * @param {Number} width
   * @param {Number} height
   * @return {ColorPicker} Returns itself for chaining purpose
   */
  public setSize(width: number, height: number): ColorPicker {
    this.width = width
    this.height = height
    this.$el.style.width = this.width + this._widthUnits
    this.$el.style.height = this.height + this._heightUnits
    this._saturationWidth = this.width - 25
    this.$saturation.style.width = this._saturationWidth + 'px'
    this._hueHeight = this.height
    this._maxHue = this._hueHeight - 2
    return this
  }
  /**
   * Set the background color of the picker. It also adds a 5px padding
   * for design purpose.
   * @param {String|Number} color hex color desired for background
   * @return {ColorPicker} Returns itself for chaining purpose
   */
  public setBackgroundColor(color: string | number): ColorPicker {
    this.backgroundColor.fromHex(color)
    this.$el.style.padding = '5px'
    this.$el.style.background = this.backgroundColor.hexString
    return this
  }
  /**
   * Removes background of the picker if previously set. It's no use
   * calling this method if you didn't set the background option on start
   * or if you didn't call setBackgroundColor previously.
   */
  public setNoBackground(): ColorPicker {
    this.$el.style.padding = '0px'
    this.$el.style.background = 'none'
    return this
  }
  /**
   * Registers callback to the update event of the picker.
   * picker inherits from [component/emitter](https://github.com/component/emitter)
   * so you could do the same thing by calling `colorPicker.on('update');`
   * @param  {Function} callback
   * @return {ColorPicker} Returns itself for chaining purpose
   */
  public onChange(callback: Function): ColorPicker {
    if (this._callbacks.indexOf(callback) < 0) {
      this._callbacks.push(callback)
      callback(this.getHexString())
    }
    return this
  }
  /**
   * Is true when mouse is down and user is currently choosing a color.
   */
  public get isChoosing(): boolean {
    return this._isChoosing
  }
  /* =============================================================================
    Color getters
  ============================================================================= */
  /**
   * Main color getter, will return a formatted color string depending on input
   * or a number depending on the last setColor call.
   * @return {Number|String}
   */
  public getColor(): number | string {
    if (this._inputIsNumber) {
      return this.getHexNumber()
    }
    return this.getHexString()
  }
  /**
   * Returns color as css hex string (ex: '#FF0000').
   * @return {String}
   */
  public getHexString(): string {
    return this.color.hexString.toUpperCase()
  }
  /**
   * Returns color as number (ex: 0xFF0000).
   * @return {Number}
   */
  public getHexNumber(): number {
    return this.color.hex
  }
  /**
   * Returns color as {r: 1, g: 0, b: 0} object.
   * @return {Object}
   */
  public getRGB(): RGBAColor {
    return this.color.rgb
  }
  /**
   * Returns color as {h: 100, s: 1, v: 1} object.
   * @return {Object}
   */
  public getHSV(): HSVAColor {
    return this.color.hsv
  }
  /**
   * Returns true if color is perceived as dark
   * @return {Boolean}
   */
  public isDark(): boolean {
    return this.color.isDark
  }
  /**
   * Returns true if color is perceived as light
   * @return {Boolean}
   */
  public isLight(): boolean {
    return this.color.isLight
  }
  /* =============================================================================
    Private methods
  ============================================================================= */
  private _moveSelectorTo(x: number, y: number): void {
    this.position.x = clamp(x, 0, this._saturationWidth)
    this.position.y = clamp(y, 0, this._hueHeight)
    this.$sbSelector.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`
  }
  private _updateColorFromPosition(): void {
    this.color.fromHsv({
      h: this.hue,
      s: this.position.x / this._saturationWidth,
      v: 1 - this.position.y / this._hueHeight
    })
    this._updateColor()
  }
  private _moveHueTo(y: number): void {
    this._huePosition = clamp(y, 0, this._maxHue)
    this.$hSelector.style.transform = `translateY(${this._huePosition}px)`
  }
  private _updateHueFromPosition(): void {
    const hsvColor = this.getHSV()
    this.hue = 1 - this._huePosition / this._maxHue
    this.color.fromHsv({ h: this.hue, s: hsvColor.s, v: hsvColor.v })
    this._updateHue()
  }
  private _updateHue(): void {
    this.hueColor.fromHsv({ h: this.hue, s: 1, v: 1 })
    this.$saturation.style.background = `linear-gradient(to right, #fff, ${this.hueColor.hexString})`
    this._updateColor()
  }
  private _updateColor(): void {
    this.$sbSelector.style.background = this.getHexString()
    this.$sbSelector.style.borderColor = this.isDark() ? '#fff' : '#000'
    this._triggerChange()
  }
  private _triggerChange(): void {
    this._callbacks.forEach(callback => callback(this.getHexString()))
  }
  /* =============================================================================
    Events handlers
  ============================================================================= */
  private _onSaturationMouseDown = (e: MouseEvent | TouchEvent): void => {
    const sbOffset = this.$saturation.getBoundingClientRect()
    const { x, y } = getMousePosition(e)
    this._isChoosing = true
    this._moveSelectorTo(x - sbOffset.left, y - sbOffset.top)
    this._updateColorFromPosition()
    this._window.addEventListener('mouseup', this._onSaturationMouseUp)
    this._window.addEventListener('touchend', this._onSaturationMouseUp)
    this._window.addEventListener('mousemove', this._onSaturationMouseMove)
    this._window.addEventListener('touchmove', this._onSaturationMouseMove)
    e.preventDefault()
  }
  private _onSaturationMouseMove = (e: MouseEvent | TouchEvent): void => {
    const sbOffset = this.$saturation.getBoundingClientRect()
    const { x, y } = getMousePosition(e)
    this._moveSelectorTo(x - sbOffset.left, y - sbOffset.top)
    this._updateColorFromPosition()
  }
  private _onSaturationMouseUp = () => {
    this._isChoosing = false
    this._window.removeEventListener('mouseup', this._onSaturationMouseUp)
    this._window.removeEventListener('touchend', this._onSaturationMouseUp)
    this._window.removeEventListener('mousemove', this._onSaturationMouseMove)
    this._window.removeEventListener('touchmove', this._onSaturationMouseMove)
  }
  private _onHueMouseDown = (e: MouseEvent | TouchEvent): void => {
    const hOffset = this.$hue.getBoundingClientRect()
    const { y } = getMousePosition(e)
    this._isChoosing = true
    this._moveHueTo(y - hOffset.top)
    this._updateHueFromPosition()
    this._window.addEventListener('mouseup', this._onHueMouseUp)
    this._window.addEventListener('touchend', this._onHueMouseUp)
    this._window.addEventListener('mousemove', this._onHueMouseMove)
    this._window.addEventListener('touchmove', this._onHueMouseMove)
    e.preventDefault()
  }
  private _onHueMouseMove = e => {
    const hOffset = this.$hue.getBoundingClientRect()
    const { y } = getMousePosition(e)
    this._moveHueTo(y - hOffset.top)
    this._updateHueFromPosition()
  }
  private _onHueMouseUp = () => {
    this._isChoosing = false
    this._window.removeEventListener('mouseup', this._onHueMouseUp)
    this._window.removeEventListener('touchend', this._onHueMouseUp)
    this._window.removeEventListener('mousemove', this._onHueMouseMove)
    this._window.removeEventListener('touchmove', this._onHueMouseMove)
  }
}