import { runCode } from 'shrimp'

const buffer: string[] = []
const NOSPACE_TOKEN = '!!ribbit-nospace!!'
const TAG_TOKEN = '!!ribbit-tag!!'
const SELF_CLOSING = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]
const HTML5_TAGS = [
  "a", "abbr", "address", "area", "article", "aside", "audio", "b", "base",
  "bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption",
  "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del",
  "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset",
  "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5",
  "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img",
  "input", "ins", "kbd", "label", "legend", "li", "link", "main", "map",
  "mark", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup",
  "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt",
  "ruby", "s", "samp", "script", "section", "select", "slot", "small",
  "source", "span", "strong", "style", "sub", "summary", "sup", "svg",
  "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead",
  "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"
]

export async function wrapAndRunCode(code: string, globals?: Record<string, any>): Promise<any> {
  return await runCode("ribbit do:\n  " + code + "\nend", Object.assign({}, ribbitGlobals, globals))
}

export const ribbitGlobals = {
  ribbit: async (cb: Function) => {
    await cb()
    const output = buffer.join("\n")
    buffer.length = 0
    return output
  },
  tag: async (tagFn: Function, atDefaults = {}) =>
    (atNamed = {}, ...args: any[]) => tagFn(Object.assign({}, atDefaults, atNamed), ...args),
  nospace: () => NOSPACE_TOKEN,
  echo: (...args: any[]) => console.log(...args)
}

for (const name of HTML5_TAGS) {
  (ribbitGlobals as any)[name] = (atNamed: {}, ...args: any[]) => tag(name, atNamed, ...args)
    ; (ribbitGlobals as any)[name].tagName = name
}

const tagBlock = async (tagName: string, props = {}, fn: Function) => {
  const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`)
  const space = attrs.length ? ' ' : ''

  buffer.push(`<${tagName}${space}${attrs.join(' ')}>`)
  await fn()
  buffer.push(`</${tagName}>`)
}

const tagCall = (tagName: string, atNamed = {}, ...args: any[]) => {
  args = args.map(arg => typeof arg === 'function' ? arg.tagName : arg)

  const attrs = Object.entries(atNamed).map(([key, value]) => `${key}="${value}"`)
  const space = attrs.length ? ' ' : ''
  const children = args
    .reverse()
    .map(a => a === TAG_TOKEN ? buffer.pop() : a)
    .reverse().join(' ')
    .replaceAll(` ${NOSPACE_TOKEN} `, '')

  if (SELF_CLOSING.includes(tagName))
    buffer.push(`<${tagName}${space}${attrs.join(' ')} />`)
  else
    buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}</${tagName}>`)
}

const tag = async (tagName: string, atNamed = {}, ...args: any[]) => {
  if (typeof args[0] === 'function')
    await tagBlock(tagName, atNamed, args[0])
  else
    tagCall(tagName, atNamed, ...args)

  return TAG_TOKEN
}
