import './register'
import * as dom from './dom'
import {__, component} from 'riot'
import curry from 'curri'

const {CSS_BY_NAME} = __.cssManager
const DOCTYPE_DECLARATION = '<!doctype html>'
const INPUT_ELEMENTS_SELECTOR = 'input,textarea,select,option'
const INPUT_PASSWORD_TYPE = 'password'
const ATTRIBUTE_TYPE_NAME = 'type'
const VALUE_ATTRIBUTE = 'value'

// check if a DOM node has the attribute type='password'
const hasPasswordAttributeType = el => el.attributes.some(attr => attr.name === ATTRIBUTE_TYPE_NAME && attr.value === INPUT_PASSWORD_TYPE)
// defer the some callbacks if the rendering is async
const defer = callback => global.window && global.window.requestAnimationFrame ? global.window.requestAnimationFrame(callback) : setTimeout(callback)
// call the dispose method asynchronously for the async rendering
const handleDisposeCallback = (isAsync, dispose) => isAsync ? defer(dispose) : dispose()

/**
 * Set the value attribute of all the inputs
 * @param {HTMLElement} element - root node
 * @return {HTMLElement[]} list of the matched input elements
 */
function setUserInputAttributes(element) {
  return element.$$(INPUT_ELEMENTS_SELECTOR).map(el => {
    const value = hasPasswordAttributeType(el) ? '' : el.value
    el.setAttribute(VALUE_ATTRIBUTE, value || '')

    return el
  })
}

/**
 * Create the renderer function that can produce different types of output from the DOM rendered
 * @param   {Function} renderer - rendering function
 * @param   {string} tagName - tag name of the root node
 * @param   {RiotComponentShell} componentAPI - component shell object
 * @param   {Object} props - initial props
 * @returns {*} output generated by the renderer function
 */
export function createRenderer(
  renderer,
  tagName,
  componentAPI,
  props = {}
) {
  dom.create()

  const isRootNode = tagName === 'html'
  const root = document.createElement(tagName)
  const element = component(componentAPI)(root, {
    ...props,
    isServer: true
  })
  const dispose = () => {
    // unmount the component
    element.unmount()
    // remove the old stored css
    CSS_BY_NAME.clear()
    // clear global scope
    dom.clear()
  }

  //reflect input value prop to attribute
  setUserInputAttributes(element)

  return renderer({
    // serialize the component outer html
    getHTML: () => `${isRootNode ? DOCTYPE_DECLARATION : ''}${root.outerHTML}`,
    dispose,
    element,
    // serialize all the generated css
    css: [...CSS_BY_NAME.values()].join('\n')
  })
}

/**
 * Get only the html string from a renderer function
 * @param   {Function} options.getHTML - function getting the component html
 * @param   {Function} options.dispose - teardown function
 * @param   {boolean} isAsync - true if the rendering was handled asynchronously
 * @returns {string} component html
 */
const getOnlyHTMLFromRenderer = ({getHTML, dispose}, isAsync) => {
  const html = getHTML()
  handleDisposeCallback(isAsync, dispose)
  return html
}

/**
 * Get only the html string from a renderer function
 * @param   {Function} options.getHTML - function getting the component html
 * @param   {Function} options.dispose - teardown function
 * @param   {string} options.css - component css
 * @param   {boolean} isAsync - true if the rendering was handled asynchronously
 * @returns {Object} {html, css} html and css of the rendered component
 */
const getFragmentsFromRenderer = ({getHTML, dispose, css}, isAsync) => {
  const html = getHTML()
  handleDisposeCallback(isAsync, dispose)
  return {html, css}
}

/**
 * Async component rendering implementation
 * @param   {Function} renderer - rendering function
 * @param   {Object} rendererPayload - renderer function payload argument
 * @returns {Promise} value of the renderer function
 */
const renderComponentAsync = curry((renderer, rendererPayload) => {
  const {element} = rendererPayload

  if (!element.onAsyncRendering)
    throw new Error('Please provide the onAsyncRendering callback to SSR asynchronously your components')

  const promise = new Promise((resolve, reject) => {
    const ret = element.onAsyncRendering(resolve, reject)

    if (ret && ret.then) {
      ret.then(resolve, reject)
    }
  })

  return Promise.race([
    promise.then(() => renderer(rendererPayload)),
    new Promise((resolve, reject) => {
      setTimeout(function() {
        reject(new Error(`Timeout error:: the component "${element.name}" didn't resolve the "onAsyncRendering" promise during the rendering process`))
      }, asyncRenderTimeout)
    })
  ])
})

export const asyncRenderTimeout = 3000
export const domGlobals = dom
export const renderAsync = curry(createRenderer)(
  renderComponentAsync(rendererPayload => getOnlyHTMLFromRenderer(rendererPayload, true))
)
export const renderAsyncFragments = curry(createRenderer)(
  renderComponentAsync(rendererPayload => getFragmentsFromRenderer(rendererPayload, true))
)
export const fragments = curry(createRenderer)(getFragmentsFromRenderer)
export const render = curry(createRenderer)(getOnlyHTMLFromRenderer)

export const { configure } = dom

export default render
