import { ComponentType, ComponentClass, FunctionalComponent } from './component'
import { EMPTY_OBJ, isString } from '@vue/shared'
import { VNode } from './vdom'
import { Data } from './componentOptions'

let stack: VNode[] = []

type TraceEntry = {
  type: VNode
  recurseCount: number
}

type ComponentTraceStack = TraceEntry[]

export function pushWarningContext(vnode: VNode) {
  stack.push(vnode)
}

export function popWarningContext() {
  stack.pop()
}

export function warn(msg: string, ...args: any[]) {
  // TODO warn handler
  console.warn(`[Vue warn]: ${msg}`, ...args)
  const trace = getComponentTrace()
  if (console.groupCollapsed) {
    trace.forEach((entry, i) => {
      const formatted = formatTraceEntry(entry, i)
      if (i === 0) {
        console.groupCollapsed('at', ...formatted)
      } else {
        console.log(...formatted)
      }
    })
    console.groupEnd()
  } else {
    const logs: string[] = []
    trace.forEach((entry, i) => {
      const formatted = formatTraceEntry(entry, i)
      if (i === 0) {
        logs.push('at', ...formatted)
      } else {
        logs.push('\n', ...formatted)
      }
    })
    console.log(...logs)
  }
}

function getComponentTrace(): ComponentTraceStack {
  let current: VNode | null | undefined = stack[stack.length - 1]
  if (!current) {
    return []
  }

  // we can't just use the stack because it will be incomplete during updates
  // that did not start from the root. Re-construct the parent chain using
  // contextVNode information.
  const normlaizedStack: ComponentTraceStack = []

  while (current) {
    const last = normlaizedStack[0]
    if (last && last.type === current) {
      last.recurseCount++
    } else {
      normlaizedStack.push({
        type: current,
        recurseCount: 0
      })
    }
    current = current.contextVNode
  }

  return normlaizedStack
}

function formatTraceEntry(
  { type, recurseCount }: TraceEntry,
  depth: number = 0
): string[] {
  const padding = depth === 0 ? '' : ' '.repeat(depth * 2 + 1)
  const postfix =
    recurseCount > 0 ? `... (${recurseCount} recursive calls)` : ``
  const open = padding + `<${formatComponentName(type.tag as ComponentType)}`
  const close = `>` + postfix
  return type.data ? [open, ...formatProps(type.data), close] : [open + close]
}

const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string =>
  str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')

function formatComponentName(c: ComponentType, file?: string): string {
  let name: string

  if (c.prototype && c.prototype.render) {
    // stateful
    const cc = c as ComponentClass
    const options = cc.options || EMPTY_OBJ
    name = options.displayName || cc.name
  } else {
    // functional
    const fc = c as FunctionalComponent
    name = fc.displayName || fc.name
  }

  if (file && name === 'AnonymousComponent') {
    const match = file.match(/([^/\\]+)\.vue$/)
    if (match) {
      name = match[1]
    }
  }

  return classify(name)
}

function formatProps(props: Data) {
  const res = []
  for (const key in props) {
    const value = props[key]
    if (isString(value)) {
      res.push(`${key}=${JSON.stringify(value)}`)
    } else {
      res.push(`${key}=`, value)
    }
  }
  return res
}
