import { Component } from './Component'
import { CONTEXT } from './consts/attributeNames'
import { MIDDLEWARES } from './consts/optionNames'
import { PROPS } from './consts/vNodeAttributeNames'
import { createGlueNodeByElement } from './createGlueNodeByElement'
import { deepGet } from './deepGet'
import { deepSet } from './deepSet'
import { GlueNode } from './GlueNode'
import { mutateProvider } from './mutateProvider'
import { nowaitProvider } from './nowaitProvider'
import { remodelProps } from './remodelProps'
import { rendererProvider } from './rendererProvider'
import { rendering } from './rendering'

/**
 * atto
 *
 * @param  view Component
 * @param  containerOrGlueNode Element | GlueNode
 * @param  options object default: `{}`
 * @return (context: any, path?: string) => void
 */
export function atto (
  view: Component,
  containerOrGlueNode: Element | GlueNode,
  options: any = {}
) {
  let scheduled = false
  let renderNow = false
  let rerender = false

  let glueNode =
    containerOrGlueNode instanceof Element
      ? createGlueNodeByElement(containerOrGlueNode)
      : (containerOrGlueNode as GlueNode)

  const rootProps = remodelProps(glueNode[PROPS])

  let rootContext: any = deepGet(rootProps, CONTEXT)

  const middlewares = (MIDDLEWARES in options && options[MIDDLEWARES]) || []

  const mutate = mutateProvider(getContext, setContext, scheduleRender)

  const [nowait, nowaitUnsubscribe] = nowaitProvider(scheduleRender)

  const extra = { mutate, nowait }

  const rendererProviders = [rendererProvider]
    .concat(middlewares)
    .map((provider: Function) =>
      provider(mutate, getContext, setContext, view, glueNode)
    )

  function getContext (path: string, def: any = {}) {
    return (path ? deepGet(rootContext, path) : rootContext) || def
  }

  function setContext (newContext: any, path?: string) {
    if (path) {
      deepSet(rootContext, path, newContext)
    } else {
      rootContext = newContext
    }
  }

  function render () {
    renderNow = true

    glueNode = rendering(
      glueNode,
      view,
      extra,
      rendererProviders.map((provider) => provider())
    )
  }

  function rendered () {
    renderNow = scheduled = false
    if (rerender) {
      rerender = false
      scheduleRender()
    } else {
      nowaitUnsubscribe()
    }
  }

  function renderedError (e) {
    rendered()
    throw e
  }

  function scheduleRender () {
    if (!scheduled) {
      scheduled = true
      Promise.resolve()
        .then(render)
        .then(rendered, renderedError)
    } else if (renderNow) {
      rerender = true
    }
  }

  return mutate
}
