/**
 * `@virtuoso.dev/react-urx` exports the [[systemToComponent]] function.
 * It wraps urx systems in to UI **logic provider components**,
 * mapping the system input and output streams to the component input / output points.
 *
 * ### Simple System wrapped as React Component
 *
 * ```tsx
 * const sys = system(() => {
 *   const foo = statefulStream(42)
 *   return { foo }
 * })
 *
 * const { Component: MyComponent, useEmitterValue } = systemToComponent(sys, {
 *   required: { fooProp: 'foo' },
 * })
 *
 * const Child = () => {
 *   const foo = useEmitterValue('foo')
 *   return <div>{foo}</div>
 * }
 *
 * const App = () => {
 *   return <Comp fooProp={42}><Child /><Comp>
 * }
 * ```
 *
 * @packageDocumentation
 */
import * as React from 'react'
import {
  ComponentType,
  createContext,
  createElement,
  forwardRef,
  ForwardRefExoticComponent,
  ReactNode,
  RefAttributes,
  useContext,
  useImperativeHandle,
  useState,
  useCallback,
} from 'react'
import {
  AnySystemSpec,
  reset,
  curry1to0,
  curry2to1,
  Emitter,
  SR,
  eventHandler,
  getValue,
  publish,
  Publisher,
  init,
  StatefulStream,
  Stream,
  subscribe,
  always,
  tap,
} from '@virtuoso.dev/urx'

/** @internal */
interface Dict<T> {
  [key: string]: T
}

/** @internal */
function omit<O extends Dict<any>, K extends readonly string[]>(keys: K, obj: O): Omit<O, K[number]> {
  var result = {} as Dict<any>
  var index = {} as Dict<1>
  var idx = 0
  var len = keys.length

  while (idx < len) {
    index[keys[idx]] = 1
    idx += 1
  }

  for (var prop in obj) {
    if (!index.hasOwnProperty(prop)) {
      result[prop] = obj[prop]
    }
  }

  return result as any
}

const useIsomorphicLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect

/** @internal */
export type Observable<T> = Emitter<T> | Publisher<T>

/**
 * Describes the mapping between the system streams and the component properties.
 * Each property uses the keys as the names of the properties and the values as the corresponding stream names.
 * @typeParam SS the type of the system.
 */
export interface SystemPropsMap<SS extends AnySystemSpec, K = keyof SR<SS>, D = { [key: string]: K }> {
  /**
   * Specifies the required component properties.
   */
  required?: D
  /**
   * Specifies the optional component properties.
   */
  optional?: D
  /**
   * Specifies the component methods, if any. Streams are converted to methods with a single argument.
   * When invoked, the method publishes the value of the argument to the specified stream.
   */
  methods?: D
  /**
   * Specifies the component "event" properties, if any.
   * Event properties accept callback functions which get executed when the stream emits a new value.
   */
  events?: D
}

/** @internal */
export type PropsFromPropMap<E extends AnySystemSpec, M extends SystemPropsMap<E>> = {
  [K in Extract<keyof M['required'], string>]: M['required'][K] extends string
    ? SR<E>[M['required'][K]] extends Observable<infer R>
      ? R
      : never
    : never
} &
  {
    [K in Extract<keyof M['optional'], string>]?: M['optional'][K] extends string
      ? SR<E>[M['optional'][K]] extends Observable<infer R>
        ? R
        : never
      : never
  } &
  {
    [K in Extract<keyof M['events'], string>]?: M['events'][K] extends string
      ? SR<E>[M['events'][K]] extends Observable<infer R>
        ? (value: R) => void
        : never
      : never
  }

/** @internal */
export type MethodsFromPropMap<E extends AnySystemSpec, M extends SystemPropsMap<E>> = {
  [K in Extract<keyof M['methods'], string>]: M['methods'][K] extends string
    ? SR<E>[M['methods'][K]] extends Observable<infer R>
      ? (value: R) => void
      : never
    : never
}

/**
 * Used to correctly specify type refs for system components
 *
 * ```tsx
 * const s = system(() => { return { a: statefulStream(0) } })
 * const { Component } = systemToComponent(s)
 *
 * const App = () => {
 *  const ref = useRef<RefHandle<typeof Component>>()
 *  return <Component ref={ref} />
 * }
 * ```
 *
 * @typeParam T the type of the component
 */
export type RefHandle<T> = T extends ForwardRefExoticComponent<RefAttributes<infer Handle>> ? Handle : never

/**
 * Converts a system spec to React component by mapping the system streams to component properties, events and methods. Returns hooks for querying and modifying
 * the system streams from the component's child components.
 * @param systemSpec The return value from a [[system]] call.
 * @param map The streams to props / events / methods mapping Check [[SystemPropsMap]] for more details.
 * @param Root The optional React component to render. By default, the resulting component renders nothing, acting as a logical wrapper for its children.
 * @returns an object containing the following:
 *  - `Component`: the React component.
 *  - `useEmitterValue`: a hook that lets child components use values emitted from the specified output stream.
 *  - `useEmitter`: a hook that calls the provided callback whenever the specified stream emits a value.
 *  - `usePublisher`: a hook which lets child components publish values to the specified stream.
 *  <hr />
 */
export function systemToComponent<SS extends AnySystemSpec, M extends SystemPropsMap<SS>, S extends SR<SS>, R>(
  systemSpec: SS,
  map: M,
  Root?: R
) {
  const requiredPropNames = Object.keys(map.required || {})
  const optionalPropNames = Object.keys(map.optional || {})
  const methodNames = Object.keys(map.methods || {})
  const eventNames = Object.keys(map.events || {})
  const Context = createContext<SR<SS>>(({} as unknown) as any)

  type RootCompProps = R extends ComponentType<infer RP> ? RP : { children?: ReactNode }

  type CompProps = PropsFromPropMap<SS, M> & RootCompProps

  type CompMethods = MethodsFromPropMap<SS, M>

  function applyPropsToSystem(system: ReturnType<SS['constructor']>, props: any) {
    if (system['propsReady']) {
      publish(system['propsReady'], false)
    }

    for (const requiredPropName of requiredPropNames) {
      const stream = system[map.required![requiredPropName]]
      publish(stream, (props as any)[requiredPropName])
    }

    for (const optionalPropName of optionalPropNames) {
      if (optionalPropName in props) {
        const stream = system[map.optional![optionalPropName]]
        publish(stream, (props as any)[optionalPropName])
      }
    }

    if (system['propsReady']) {
      publish(system['propsReady'], true)
    }
  }

  function buildMethods(system: ReturnType<SS['constructor']>) {
    return methodNames.reduce((acc, methodName) => {
      ;(acc as any)[methodName] = (value: any) => {
        const stream = system[map.methods![methodName]]
        publish(stream, value)
      }
      return acc
    }, {} as CompMethods)
  }

  function buildEventHandlers(system: ReturnType<SS['constructor']>) {
    return eventNames.reduce((handlers, eventName) => {
      handlers[eventName] = eventHandler(system[map.events![eventName]])
      return handlers
    }, {} as { [key: string]: Emitter<any> })
  }

  /**
   * A React component generated from an urx system
   */
  const Component = forwardRef<CompMethods, CompProps>((propsWithChildren, ref) => {
    const { children, ...props } = propsWithChildren as any

    const [system] = useState(() => {
      return tap(init(systemSpec), system => applyPropsToSystem(system, props))
    })

    const [handlers] = useState(curry1to0(buildEventHandlers, system))

    useIsomorphicLayoutEffect(() => {
      for (const eventName of eventNames) {
        if (eventName in props) {
          subscribe(handlers[eventName], props[eventName])
        }
      }
      return () => {
        Object.values(handlers).map(reset)
      }
    }, [props, handlers, system])

    useIsomorphicLayoutEffect(() => {
      applyPropsToSystem(system, props)
    })

    useImperativeHandle(ref, always(buildMethods(system)))

    return createElement(
      Context.Provider,
      { value: system },
      Root
        ? createElement(
            (Root as unknown) as ComponentType,
            omit([...requiredPropNames, ...optionalPropNames, ...eventNames], props),
            children
          )
        : children
    )
  })

  const usePublisher = <K extends keyof S>(key: K) => {
    return useCallback(curry2to1(publish, React.useContext(Context)[key]), [key]) as (
      value: S[K] extends Stream<infer R> ? R : never
    ) => void
  }

  /**
   * Returns the value emitted from the stream.
   */
  const useEmitterValue = <K extends keyof S, V = S[K] extends StatefulStream<infer R> ? R : never>(key: K) => {
    const context = useContext(Context)
    const source: StatefulStream<V> = context[key]

    const [value, setValue] = useState(curry1to0(getValue, source))

    useIsomorphicLayoutEffect(
      () =>
        subscribe(source, (next: V) => {
          if (next !== value) {
            setValue(always(next))
          }
        }),
      [source, value]
    )

    return value
  }

  const useEmitter = <K extends keyof S, V = S[K] extends Stream<infer R> ? R : never>(key: K, callback: (value: V) => void) => {
    const context = useContext(Context)
    const source: Stream<V> = context[key]
    useIsomorphicLayoutEffect(() => subscribe(source, callback), [callback, source])
  }

  return {
    Component,
    usePublisher,
    useEmitterValue,
    useEmitter,
  }
}
