import type { Token, HTMLNode, TagToken, NormalElement, TagEndToken, AttributeToken, TextToken } from './types'
import { closingTags, closingTagAncestorBreakers, voidTags } from './tags'

interface StackItem {
  tagName: string | null
  children: HTMLNode[]
}

interface State {
  stack: StackItem[]
  cursor: number
  tokens: Token[]
}

export const parser = (tokens: Token[]) => {
  const root: StackItem = { tagName: null, children: [] }
  const state: State = { tokens, cursor: 0, stack: [root] }
  parse(state)
  return root.children
}

export const hasTerminalParent = (tagName: string, stack: StackItem[]) => {
  const tagParents = closingTagAncestorBreakers[tagName]
  if (tagParents) {
    let currentIndex = stack.length - 1
    while (currentIndex >= 0) {
      const parentTagName = stack[currentIndex].tagName
      if (parentTagName === tagName) break
      if (parentTagName && tagParents.includes(parentTagName)) return true
      currentIndex--
    }
  }
  return false
}

export const rewindStack = (stack: StackItem[], newLength: number) => {
  stack.splice(newLength)
}

export const parse = (state: State) => {
  const { stack, tokens } = state
  let { cursor } = state
  let nodes = stack[stack.length - 1].children
  const len = tokens.length
  
  while (cursor < len) {
    const token = tokens[cursor]
    if (token.type !== 'tag-start') {
      nodes.push(token as TextToken)
      cursor++
      continue
    }

    const tagToken = tokens[++cursor] as TagToken
    cursor++
    const tagName = tagToken.content.toLowerCase()
    if (token.close) {
      let index = stack.length
      let shouldRewind = false
      while (--index > -1) {
        if (stack[index].tagName === tagName) {
          shouldRewind = true
          break
        }
      }
      while (cursor < len) {
        if (tokens[cursor].type !== 'tag-end') break
        cursor++
      }
      if (shouldRewind) {
        rewindStack(stack, index)
        break
      } 
      else continue
    }

    const isClosingTag = closingTags.includes(tagName)
    let shouldRewindToAutoClose = isClosingTag
    if (shouldRewindToAutoClose) {
      shouldRewindToAutoClose = !hasTerminalParent(tagName, stack)
    }

    if (shouldRewindToAutoClose) {
      let currentIndex = stack.length - 1
      while (currentIndex > 0) {
        if (tagName === stack[currentIndex].tagName) {
          rewindStack(stack, currentIndex)
          const previousIndex = currentIndex - 1
          nodes = stack[previousIndex].children
          break
        }
        currentIndex = currentIndex - 1
      }
    }

    const attributes = []
    let tagEndToken: TagEndToken | undefined
    while (cursor < len) {
      const _token = tokens[cursor]
      if (_token.type === 'tag-end') {
        tagEndToken = _token
        break
      }
      attributes.push((_token as AttributeToken).content)
      cursor++
    }

    if (!tagEndToken) break

    cursor++
    const children: HTMLNode[] = []
    const elementNode: NormalElement = {
      type: 'element',
      tagName: tagToken.content,
      attributes,
      children,
    }
    nodes.push(elementNode)

    const hasChildren = !(tagEndToken.close || voidTags.includes(tagName))
    if (hasChildren) {
      stack.push({tagName, children})
      const innerState = { tokens, cursor, stack }
      parse(innerState)
      cursor = innerState.cursor
    }
  }
  state.cursor = cursor
}