import { Component, Stream } from '@motorcycle/types'
import { DomSinks, DomSources } from './'
import { curry3, join } from '@typed/prelude'

import { VNode } from 'mostly-dom'
import { tap } from '@motorcycle/stream'

/**
 * Isolates a component by adding an isolation class name to the outermost
 * DOM element emitted by the component’s view stream.
 *
 * The isolation class name is generated by appending the given isolation `key`
 * to the prefix `$$isolation$$-`, e.g., given `foo` as `key` produces
 * `$$isolation$$-foo`.
 *
 * Isolating components are useful especially when dealing with lists of a
 * specific component, so that events can be differentiated between the siblings.
 * However, isolated components are not isolated from access by an ancestor DOM
 * element.
 *
 * Note that `isolate` is curried.
 *
 * @name isolate<Sources extends DomSources, Sinks extends DomSinks>(component: Component<Sources, Sinks>, key: string, sources: Sources): Sinks
 *
 * @example
 * import { empty } from '@motorcycle/stream'
 * import { createDomSource } from '@motorcycle/dom'
 *
 * const sources = createDomSource(empty())
 * const sinks = isolate(MyComponent, `myIsolationKey`, sources)
 */
export const isolate: IsolatedComponent = curry3(function isolate<
  Sources extends DomSources,
  Sinks extends DomSinks
>(component: Component<Sources, Sinks>, key: string, sources: Sources): Sinks {
  const { dom } = sources
  const isolatedDom = dom.query(`.${KEY_PREFIX}${key}`)
  const sinks = component(Object.assign({}, sources, { dom: isolatedDom }))
  const isolatedSinks = Object.assign({}, sinks, { view$: isolateView(sinks.view$, key) })

  return isolatedSinks
})

const KEY_PREFIX = `__isolation__`

function isolateView(view$: Stream<VNode>, key: string) {
  const prefixedKey = KEY_PREFIX + key

  return tap(vNode => {
    const { props: { className: className = EMPTY_CLASS_NAME } } = vNode
    const needsIsolation = className.indexOf(prefixedKey) === -1

    if (needsIsolation)
      vNode.props.className = removeSuperfluousSpaces(
        join(CLASS_NAME_SEPARATOR, [className, prefixedKey])
      )
  }, view$)
}

const EMPTY_CLASS_NAME = ``
const CLASS_NAME_SEPARATOR = ` `

function removeSuperfluousSpaces(str: string): string {
  return str.replace(RE_TWO_OR_MORE_SPACES, CLASS_NAME_SEPARATOR)
}

const RE_TWO_OR_MORE_SPACES = /\s{2,}/g

export interface IsolatedComponent {
  <Sources extends DomSources, Sinks extends DomSinks>(
    component: Component<Sources, Sinks>,
    key: string,
    sources: Sources
  ): Sinks
  <Sources extends DomSources, Sinks extends DomSinks>(
    component: Component<Sources, Sinks>,
    key: string
  ): Component<Sources, Sinks>
  <Sources extends DomSources, Sinks extends DomSinks>(
    component: Component<Sources, Sinks>
  ): IsolatedComponentArity2<Sources, Sinks>
}

export interface IsolatedComponentArity2<Sources extends DomSources, Sinks extends DomSinks> {
  (key: string, sources: Sources): Sinks
  (key: string): Component<Sources, Sinks>
}
