import Editor from '../editor/index'
import { renderFormula } from '../mathjax'
import { toArray } from './util'

// 记录元素基于上一个相对&绝对定位的位置信息
type OffsetDataType = {
  top: number
  left: number
  width: number
  height: number
  parent: Element | null
}

type EventItem = {
  elem: HTMLElement
  selector: string
  fn: listener
  agentFn: listener
}
const AGENT_EVENTS: EventItem[] = []

/**
 * 根据 html 字符串创建 elem
 * @param {String} html html
 */
function _createElemByHTML(html: string): HTMLElement[] {
  const div = document.createElement('div')
  div.innerHTML = html
  const elems = div.children
  return toArray(elems)
}

/**
 * 判断是否是 DOM List
 * @param selector DOM 元素或列表
 */
function _isDOMList<T extends HTMLCollection | NodeList>(
  selector: unknown,
): selector is T {
  if (!selector) {
    return false
  }
  if (selector instanceof HTMLCollection || selector instanceof NodeList) {
    return true
  }
  return false
}

/**
 * 封装 querySelectorAll
 * @param selector css 选择器
 */
function _querySelectorAll(selector: string): HTMLElement[] {
  const elems = document.querySelectorAll(selector)
  return toArray(elems)
}

/**
 * 封装 _styleArrTrim
 * @param styleArr css
 */
function _styleArrTrim(style: string | string[]): string[] {
  let styleArr: string[] = []
  const resultArr: string[] = []

  if (!Array.isArray(style)) {
    // 有 style，将 style 按照 `;` 拆分为数组
    styleArr = style.split(';')
  } else {
    styleArr = style
  }

  styleArr.forEach((item) => {
    // 对每项样式，按照 : 拆分为 key 和 value
    const arr = item.split(':').map((i) => {
      return i.trim()
    })
    if (arr.length === 2) {
      resultArr.push(arr[0] + ':' + arr[1])
    }
  })
  return resultArr
}

export type DomElementSelector =
  | string
  | DomElement
  | Document
  | Node
  | NodeList
  | ChildNode
  | ChildNode[]
  | Element
  | HTMLElement
  | HTMLElement[]
  | HTMLCollection
  | EventTarget
  | null
  | undefined

// 构造函数
export class DomElement<T extends DomElementSelector = DomElementSelector> {
  // 定义属性
  selector!: T
  length: number
  elems: HTMLElement[]
  dataSource: Map<string, any>
  prior?: DomElement // 通过 getNodeTop 获取顶级段落的时候，可以通过 prior 去回溯来源的子节点

  /**
   * 构造函数
   * @param selector 任一类型的选择器
   */
  constructor(selector: T) {
    // 初始化属性
    this.elems = []
    this.length = this.elems.length
    this.dataSource = new Map()

    if (!selector) {
      return
    }

    // 原本就是 DomElement 实例，则直接返回
    if (selector instanceof DomElement) {
      return selector as DomElement<T>
    }

    let selectorResult: HTMLElement[] = [] // 存储查询结果
    const nodeType = selector instanceof Node ? selector.nodeType : -1
    this.selector = selector

    if (nodeType === 1 || nodeType === 9) {
      selectorResult = [selector as HTMLElement]
    } else if (_isDOMList(selector)) {
      // DOM List
      selectorResult = toArray(selector)
    } else if (selector instanceof Array) {
      // Element 数组（其他数据类型，暂时忽略）
      selectorResult = selector as HTMLElement[]
    } else if (typeof selector === 'string') {
      // 字符串
      const tmpSelector = selector.replace('/\n/mg', '').trim()
      if (tmpSelector.indexOf('<') === 0) {
        // 如 <div>
        selectorResult = _createElemByHTML(tmpSelector)
      } else {
        // 如 #id .class
        selectorResult = _querySelectorAll(tmpSelector)
      }
    }

    const length = selectorResult.length
    if (!length) {
      // 空数组
      return this
    }

    // 加入 DOM 节点
    let i = 0
    for (; i < length; i++) {
      this.elems.push(selectorResult[i])
    }
    this.length = length
  }

  /**
   * 获取元素 id
   */
  get id(): string {
    return this.elems[0].id
  }

  /**
   * 遍历所有元素，执行回调函数
   * @param fn 回调函数
   */
  forEach(
    fn: (ele: HTMLElement, index?: number) => boolean | unknown,
  ): DomElement {
    for (let i = 0; i < this.length; i++) {
      const elem = this.elems[i]
      const result = fn.call(elem, elem, i)
      if (result === false) {
        break
      }
    }
    return this
  }

  /**
   * 克隆元素
   * @param deep 是否深度克隆
   */
  clone(deep: boolean = false): DomElement {
    const cloneList: HTMLElement[] = []
    this.elems.forEach((elem) => {
      cloneList.push(elem.cloneNode(!!deep) as HTMLElement)
    })
    return $(cloneList)
  }

  /**
   * 获取第几个元素
   * @param index index
   */
  get(index: number = 0): DomElement {
    const length = this.length
    if (index >= length) {
      index = index % length
    }
    return $(this.elems[index])
  }

  /**
   * 获取第一个元素
   */
  first(): DomElement {
    return this.get(0)
  }

  /**
   * 获取最后一个元素
   */
  last(): DomElement {
    const length = this.length
    return this.get(length - 1)
  }

  /**
   * 绑定事件
   * @param type 事件类型
   * @param selector DOM 选择器
   * @param fn 事件函数
   */
  on(type: string, fn: Function): DomElement
  on(type: string, selector: string, fn: Function): DomElement
  on(type: string, selector: string | Function, fn?: Function): DomElement {
    if (!type) return this

    // 没有 selector ，只有 type 和 fn
    if (typeof selector === 'function') {
      fn = selector
      selector = ''
    }

    return this.forEach((elem) => {
      // 没有事件代理
      if (!selector) {
        // 无代理
        elem.addEventListener(type, fn as listener)
        return
      }

      // 有事件代理
      const agentFn: listener = function (e: Event) {
        const target = e.target as HTMLElement
        if (target.matches(selector as string)) {
          ;(fn as listener).call(target, e)
        }
      }
      elem.addEventListener(type, agentFn)

      // 缓存代理事件
      AGENT_EVENTS.push({
        elem: elem,
        selector: selector as string,
        fn: fn as listener,
        agentFn,
      })
    })
  }

  /**
   * 解绑事件
   * @param type 事件类型
   * @param selector DOM 选择器
   * @param fn 事件函数
   */
  off(type: string, fn: Function): DomElement
  off(type: string, selector: string, fn: Function): DomElement
  off(type: string, selector: string | Function, fn?: Function): DomElement {
    if (!type) return this

    // 没有 selector ，只有 type 和 fn
    if (typeof selector === 'function') {
      fn = selector
      selector = ''
    }

    return this.forEach(function (elem: HTMLElement) {
      // 解绑事件代理
      if (selector) {
        let idx = -1
        for (let i = 0; i < AGENT_EVENTS.length; i++) {
          const item = AGENT_EVENTS[i]
          if (
            item.selector === selector &&
            item.fn === fn &&
            item.elem === elem
          ) {
            idx = i
            break
          }
        }
        if (idx !== -1) {
          const { agentFn } = AGENT_EVENTS.splice(idx, 1)[0]
          elem.removeEventListener(type, agentFn)
        }
      } else {
        // @ts-ignore
        elem.removeEventListener(type, fn)
      }
    })
  }

  /**
   * 设置/获取 属性
   * @param key key
   * @param val value
   */
  attr(key: string): string
  attr(key: string, val: string): DomElement
  attr(key: string, val?: string): DomElement | string {
    if (val === void 0) {
      // 获取数据
      return this.elems[0].getAttribute(key) || ''
    }

    // 否则，设置属性
    return this.forEach(function (elem: HTMLElement) {
      elem.setAttribute(key, val)
    })
  }

  /**
   * 删除 属性
   * @param key key
   */
  removeAttr(key: string): void {
    this.forEach(function (elem: HTMLElement) {
      elem.removeAttribute(key)
    })
  }

  /**
   * 添加 css class
   * @param className css class
   */
  addClass(className?: string): DomElement {
    if (!className) {
      return this
    }

    return this.forEach(function (elem: HTMLElement) {
      if (elem.className) {
        // 当前有 class
        let arr: string[] = elem.className.split(/\s/)
        arr = arr.filter((item) => {
          return !!item.trim()
        })
        // 添加 class
        if (arr.indexOf(className) < 0) {
          arr.push(className)
        }
        // 修改 elem.class
        elem.className = arr.join(' ')
      } else {
        // 当前没有 class
        elem.className = className
      }
    })
  }

  /**
   * 添加 css class
   * @param className css class
   */
  removeClass(className?: string): DomElement {
    if (!className) {
      return this
    }
    return this.forEach(function (elem: HTMLElement) {
      if (!elem.className) {
        // 当前无 class
        return
      }

      let arr: string[] = elem.className.split(/\s/)
      arr = arr.filter((item) => {
        item = item.trim()
        // 删除 class
        if (!item || item === className) {
          return false
        }
        return true
      })
      // 修改 elem.class
      elem.className = arr.join(' ')
    })
  }

  /**
   * 是否有传入的 css class
   * @param className css class
   */
  hasClass(className?: string): boolean {
    if (!className) {
      return false
    }
    const elem = this.elems[0]
    if (!elem.className) {
      // 当前无 class
      return false
    }
    const arr: string[] = elem.className.split(/\s/)
    return arr.includes(className) // 是否包含
  }

  /**
   * 修改 css
   * @param key css key
   * @param val css value
   */
  // css(key: string): string
  css(key: string, val?: string | number): DomElement {
    let currentStyle: string
    if (val === '') {
      currentStyle = ''
    } else {
      currentStyle = `${key}:${val};`
    }
    return this.forEach((elem) => {
      const style = (elem.getAttribute('style') || '').trim()
      if (style) {
        // 有 style，将 style 按照 `;` 拆分为数组
        let resultArr: string[] = _styleArrTrim(style)

        // 替换现有的 style
        resultArr = resultArr.map((item) => {
          if (item.indexOf(key) === 0) {
            return currentStyle
          } else {
            return item
          }
        })
        // 新增 style
        if (currentStyle !== '' && resultArr.indexOf(currentStyle) < 0) {
          resultArr.push(currentStyle)
        }

        // 去掉 空白
        if (currentStyle === '') {
          resultArr = _styleArrTrim(resultArr)
        }

        // 重新设置 style
        elem.setAttribute('style', resultArr.join('; '))
      } else {
        // 当前没有 style
        elem.setAttribute('style', currentStyle)
      }
    })
  }

  /**
   * 封装 getBoundingClientRect
   */
  getBoundingClientRect(): DOMRect {
    const elem = this.elems[0]
    return elem.getBoundingClientRect()
  }

  /**
   * 显示
   */
  show(): DomElement {
    return this.css('display', 'block')
  }

  /**
   * 隐藏
   */
  hide(): DomElement {
    return this.css('display', 'none')
  }

  /**
   * 获取子节点（只有 DOM 元素）
   */
  children(): DomElement | null {
    const elem = this.elems[0]
    if (!elem) {
      return null
    }

    return $(elem.children)
  }

  /**
   * 获取子节点（包括文本节点）
   */
  childNodes(): DomElement | null {
    const elem = this.elems[0]
    if (!elem) {
      return null
    }

    return $(elem.childNodes)
  }

  /**
   * 将子元素全部替换
   * @param $children 新的child节点
   */
  replaceChildAll($children: DomElement): void {
    const parent = this.getNode()
    const elem = this.elems[0]
    while (elem.hasChildNodes()) {
      parent.firstChild && elem.removeChild(parent.firstChild)
    }
    this.append($children)
  }

  /**
   * 增加子节点
   * @param $children 子节点
   */
  append($children: DomElement): DomElement {
    return this.forEach((elem) => {
      $children.forEach(function (child: HTMLElement) {
        elem.appendChild(child)
      })
    })
  }

  /**
   * 移除当前节点
   */
  remove(): DomElement {
    return this.forEach((elem) => {
      if (elem.remove) {
        elem.remove()
      } else {
        const parent = elem.parentElement
        parent && parent.removeChild(elem)
      }
    })
  }

  /**
   * 当前元素，是否包含某个子元素
   * @param $child 子元素
   */
  isContain($child: DomElement): boolean {
    const elem = this.elems[0]
    const child = $child.elems[0]
    return elem.contains(child)
  }

  /**
   * 获取当前元素 nodeName
   */
  getNodeName(): string {
    const elem = this.elems[0]
    return elem.nodeName
  }

  /**
   * 根据元素位置获取元素节点（默认获取0位置的节点）
   * @param n 元素节点位置
   */
  getNode(n: number = 0): Node {
    return this.elems[n]
  }

  /**
   * 查询
   * @param selector css 选择器
   */
  find(selector: string): DomElement {
    const elem = this.elems[0]
    return $(elem.querySelectorAll(selector))
  }

  /**
   * 获取/设置 元素 text
   * @param val text 值
   */
  text(): string
  text(val: string): DomElement
  text(val?: string): DomElement | string {
    if (val === void 0) {
      // 获取 text
      const elem = this.elems[0]

      return elem.innerHTML.replace(/<[^>]+>/g, () => '')
    } else {
      // 设置 text
      return this.forEach(function (elem: HTMLElement) {
        elem.innerHTML = val
      })
    }
  }

  /**
   * 设置/获取 元素 html
   * @param val html 值
   */
  html(): string
  html(val: string): DomElement
  html(val?: string): DomElement | string {
    const elem = this.elems[0]
    if (val === void 0) {
      // 获取 html
      return elem.innerHTML
    } else {
      // 设置 html
      elem.innerHTML = val
      return this
    }
  }

  /**
   * 获取元素 value
   */
  val(): string {
    const elem = this.elems[0]
    return (elem as any).value.trim() // 暂用 any
  }

  /**
   * focus 到当前元素
   */
  focus(): DomElement {
    return this.forEach((elem) => {
      elem.focus()
    })
  }

  /**
   * 当前元素前一个兄弟节点
   */
  prev(): DomElement {
    const elem = this.elems[0]
    return $(elem.previousElementSibling)
  }

  /**
   * 当前元素后一个兄弟节点
   * 不包括文本节点、注释节点）
   */
  next(): DomElement {
    const elem = this.elems[0]
    return $(elem.nextElementSibling)
  }

  /**
   * 获取当前节点的下一个兄弟节点
   * 包括文本节点、注释节点即回车、换行、空格、文本等等）
   */
  getNextSibling(): DomElement {
    const elem = this.elems[0]
    return $(elem.nextSibling)
  }

  /**
   * 获取父元素
   */
  parent(): DomElement {
    const elem = this.elems[0]
    return $(elem.parentElement)
  }

  /**
   * 查找父元素，直到满足 selector 条件
   * @param selector css 选择器
   * @param curElem 从哪个元素开始查找，默认为当前元素
   */
  parentUntil(selector: string, curElem?: HTMLElement): DomElement | null {
    const elem = curElem || this.elems[0]
    if (elem.nodeName === 'BODY') {
      return null
    }

    const parent = elem.parentElement
    if (!parent) return null

    if (parent.matches(selector)) {
      // 找到，并返回
      return $(parent)
    }

    // 继续查找，递归
    return this.parentUntil(selector, parent)
  }

  /**
   * 判读是否相等
   * @param $elem 元素
   */
  equal($elem: DomElement | HTMLElement): boolean {
    if ($elem instanceof DomElement) {
      return this.elems[0] === $elem.elems[0]
    } else if ($elem instanceof HTMLElement) {
      return this.elems[0] === $elem
    } else {
      return false
    }
  }

  /**
   * 将该元素插入到某个元素前面
   * @param selector css 选择器
   */
  insertBefore(selector: string | DomElement): DomElement {
    const $referenceNode = $(selector)
    const referenceNode = $referenceNode.elems[0]
    if (!referenceNode) {
      return this
    }
    return this.forEach((elem) => {
      const parent = referenceNode.parentNode as Node
      parent?.insertBefore(elem, referenceNode)
    })
  }

  /**
   * 将该元素插入到selector元素后面
   * @param selector css 选择器
   */
  insertAfter(selector: string | DomElement): DomElement {
    const $referenceNode = $(selector)
    const referenceNode = $referenceNode.elems[0]
    const anchorNode = referenceNode && referenceNode.nextSibling
    if (!referenceNode) {
      return this
    }
    return this.forEach(function (elem: HTMLElement) {
      const parent = referenceNode.parentNode as Node
      if (anchorNode) {
        parent.insertBefore(elem, anchorNode)
      } else {
        parent.appendChild(elem)
      }
    })
  }

  /**
   * 设置/获取 数据
   * @param key key
   * @param value value
   */
  data<T>(key: string, value?: T): T | undefined {
    if (value === void 0) {
      // 获取数据
      return this.dataSource.get(key)
    } else {
      // 设置数据
      this.dataSource.set(key, value)
    }
  }

  /**
   * 获取当前节点的顶级(段落)
   * @param editor 富文本实例
   */
  getNodeTop(editor: Editor): DomElement {
    // 异常抛出，空的 DomElement 直接返回
    if (this.length < 1) {
      return this
    }

    // 获取父级元素，并判断是否是 编辑区域
    // 如果是则返回当前节点
    const $parent = this.parent()

    // fix：添加当前元素与编辑区元素的比较，防止传入的当前元素就是编辑区元素而造成的获取顶级元素为空的情况
    if (
      editor.$textLatexElem.equal(this) ||
      editor.$textLatexElem.equal($parent)
    ) {
      return this
    }

    // 到了此处，即代表当前节点不是顶级段落
    // 将当前节点存放于父节点的 prior 字段下
    // 主要用于 回溯 子节点
    // 例如：ul ol 等标签
    // 实际操作的节点是 li 但是一个 ul ol 的子节点可能有多个
    // 所以需要对其进行 回溯 找到对应的子节点
    $parent.prior = this
    return $parent.getNodeTop(editor)
  }

  /**
   * 获取当前 节点 基与上一个拥有相对或者解决定位的父容器的位置
   * @param editor 富文本实例
   */
  getOffsetData(): OffsetDataType {
    const $node = this.elems[0]
    return {
      top: $node.offsetTop,
      left: $node.offsetLeft,
      width: $node.offsetWidth,
      height: $node.offsetHeight,
      parent: $node.offsetParent,
    }
  }

  /**
   * 从上至下进行滚动
   * @param top 滚动的值
   */
  scrollTop(top: number): void {
    const $node = this.elems[0]
    $node.scrollTo({ top })
  }

  renderFormula(): Promise<any> {
    return renderFormula(this.elems)
  }
}

// new 一个对象
function $(...arg: ConstructorParameters<typeof DomElement>): DomElement {
  return new DomElement(...arg)
}

export default $
