import { PopperInstance, IPopperOptions } from '@element-plus/popper'
import { getValueByPath } from '@element-plus/utils/util'
import { off, on } from '@element-plus/utils/dom'
import { createPopper } from '@popperjs/core'
import { AnyObject, TableColumnCtx } from './table.type'
import PopupManager from '@element-plus/utils/popup-manager'

export const getCell = function(event: Event): HTMLElement {
  let cell = event.target as HTMLElement

  while (cell && cell.tagName.toUpperCase() !== 'HTML') {
    if (cell.tagName.toUpperCase() === 'TD') {
      return cell
    }
    cell = cell.parentNode as HTMLElement
  }

  return null
}

const isObject = function(obj) {
  return obj !== null && typeof obj === 'object'
}

export const orderBy = function(array, sortKey, reverse, sortMethod, sortBy) {
  if (
    !sortKey &&
    !sortMethod &&
    (!sortBy || (Array.isArray(sortBy) && !sortBy.length))
  ) {
    return array
  }
  if (typeof reverse === 'string') {
    reverse = reverse === 'descending' ? -1 : 1
  } else {
    reverse = reverse && reverse < 0 ? -1 : 1
  }
  const getKey = sortMethod
    ? null
    : function(value, index) {
      if (sortBy) {
        if (!Array.isArray(sortBy)) {
          sortBy = [sortBy]
        }
        return sortBy.map(function(by) {
          if (typeof by === 'string') {
            return getValueByPath(value, by)
          } else {
            return by(value, index, array)
          }
        })
      }
      if (sortKey !== '$key') {
        if (isObject(value) && '$value' in value) value = value.$value
      }
      return [isObject(value) ? getValueByPath(value, sortKey) : value]
    }
  const compare = function(a, b) {
    if (sortMethod) {
      return sortMethod(a.value, b.value)
    }
    for (let i = 0, len = a.key.length; i < len; i++) {
      if (a.key[i] < b.key[i]) {
        return -1
      }
      if (a.key[i] > b.key[i]) {
        return 1
      }
    }
    return 0
  }
  return array
    .map(function(value, index) {
      return {
        value: value,
        index: index,
        key: getKey ? getKey(value, index) : null,
      }
    })
    .sort(function(a, b) {
      let order = compare(a, b)
      if (!order) {
        // make stable https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
        order = a.index - b.index
      }
      return order * reverse
    })
    .map(item => item.value)
}

export const getColumnById = function(
  table: {
    columns: TableColumnCtx[]
  },
  columnId: string,
): null | TableColumnCtx {
  let column = null
  table.columns.forEach(function(item) {
    if (item.id === columnId) {
      column = item
    }
  })
  return column
}

export const getColumnByKey = function(
  table: {
    columns: TableColumnCtx[]
  },
  columnKey: string,
): TableColumnCtx {
  let column = null
  for (let i = 0; i < table.columns.length; i++) {
    const item = table.columns[i]
    if (item.columnKey === columnKey) {
      column = item
      break
    }
  }
  return column
}

export const getColumnByCell = function(
  table: {
    columns: TableColumnCtx[]
  },
  cell: HTMLElement,
): null | TableColumnCtx {
  const matches = (cell.className || '').match(/el-table_[^\s]+/gm)
  if (matches) {
    return getColumnById(table, matches[0])
  }
  return null
}

export const getRowIdentity = (
  row: AnyObject,
  rowKey: string | ((row: AnyObject) => any),
): string => {
  if (!row) throw new Error('row is required when get row identity')
  if (typeof rowKey === 'string') {
    if (rowKey.indexOf('.') < 0) {
      return row[rowKey]
    }
    const key = rowKey.split('.')
    let current = row
    for (let i = 0; i < key.length; i++) {
      current = current[key[i]]
    }
    return (current as unknown) as string
  } else if (typeof rowKey === 'function') {
    return rowKey.call(null, row)
  }
}

export const getKeysMap = function(
  array: AnyObject[],
  rowKey: string,
): AnyObject {
  const arrayMap = {}
  ;(array || []).forEach((row, index) => {
    arrayMap[getRowIdentity(row, rowKey)] = { row, index }
  })
  return arrayMap
}

function hasOwn(obj: AnyObject, key: string): boolean {
  return Object.prototype.hasOwnProperty.call(obj, key)
}

export function mergeOptions<T, K>(defaults: T, config: K): T & K {
  const options = {} as T & K
  let key
  for (key in defaults) {
    options[key] = defaults[key]
  }
  for (key in config) {
    if (hasOwn(config, key)) {
      const value = config[key]
      if (typeof value !== 'undefined') {
        options[key] = value
      }
    }
  }
  return options
}

export function parseWidth(width: number | string): number | string {
  if (width !== undefined) {
    width = parseInt(width as string, 10)
    if (isNaN(width)) {
      width = null
    }
  }
  return width
}

export function parseMinWidth(minWidth): number {
  if (typeof minWidth !== 'undefined') {
    minWidth = parseWidth(minWidth)
    if (isNaN(minWidth)) {
      minWidth = 80
    }
  }
  return minWidth
}

export function parseHeight(height: number | string) {
  if (typeof height === 'number') {
    return height
  }
  if (typeof height === 'string') {
    if (/^\d+(?:px)?$/.test(height)) {
      return parseInt(height, 10)
    } else {
      return height
    }
  }
  return null
}

// https://github.com/reduxjs/redux/blob/master/src/compose.js
export function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0]
  }
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

export function toggleRowStatus(
  statusArr: AnyObject[],
  row: AnyObject,
  newVal: boolean,
): boolean {
  let changed = false
  const index = statusArr.indexOf(row)
  const included = index !== -1

  const addRow = () => {
    statusArr.push(row)
    changed = true
  }
  const removeRow = () => {
    statusArr.splice(index, 1)
    changed = true
  }

  if (typeof newVal === 'boolean') {
    if (newVal && !included) {
      addRow()
    } else if (!newVal && included) {
      removeRow()
    }
  } else {
    if (included) {
      removeRow()
    } else {
      addRow()
    }
  }
  return changed
}

export function walkTreeNode(
  root,
  cb,
  childrenKey = 'children',
  lazyKey = 'hasChildren',
) {
  const isNil = array => !(Array.isArray(array) && array.length)

  function _walker(parent, children, level) {
    cb(parent, children, level)
    children.forEach(item => {
      if (item[lazyKey]) {
        cb(item, null, level + 1)
        return
      }
      const children = item[childrenKey]
      if (!isNil(children)) {
        _walker(item, children, level + 1)
      }
    })
  }

  root.forEach(item => {
    if (item[lazyKey]) {
      cb(item, null, 0)
      return
    }
    const children = item[childrenKey]
    if (!isNil(children)) {
      _walker(item, children, 0)
    }
  })
}

export let removePopper

export function createTablePopper(
  trigger: HTMLElement,
  popperContent: string,
  popperOptions: Partial<IPopperOptions>,
) {
  function renderContent(): HTMLDivElement {
    const content = document.createElement('div')
    content.className = 'el-tooltip__popper is-dark'
    content.innerHTML = popperContent
    content.style.zIndex = String(PopupManager.nextZIndex())
    document.body.appendChild(content)
    return content
  }
  function renderArrow(): HTMLDivElement {
    const arrow = document.createElement('div')
    arrow.className = 'el-popper__arrow'
    arrow.style.bottom = '-4px'
    return arrow
  }
  function showPopper() {
    popperInstance && popperInstance.update()
  }
  removePopper = function removePopper() {
    try {
      popperInstance && popperInstance.destroy()
      content && document.body.removeChild(content)
      off(trigger, 'mouseenter', showPopper)
    } catch {}
  }
  off(trigger, 'mouseleave', removePopper)
  let popperInstance: Nullable<PopperInstance> = null
  const content = renderContent()
  const arrow = renderArrow()
  content.appendChild(arrow)

  popperInstance = createPopper(trigger, content, {
    modifiers: [
      {
        name: 'offset',
        options: {
          offset: [0, 8],
        },
      },
      {
        name: 'arrow',
        options: {
          element: arrow,
          padding: 10,
        },
      },
    ],
    ...popperOptions,
  })
  on(trigger, 'mouseenter', showPopper)
  on(trigger, 'mouseleave', removePopper)
}
