import React, { useEffect, useRef } from 'react'
import { connect as reduxConnect } from 'react-redux'

import { convertInputToLogic, convertPartialDynamicInput } from '../logic/index'
import { hasConnectWithKey } from '../logic/connect'
import { attachReducer } from '../store/reducer'

import { getCache } from '../cache'

import { mountPaths, unmountPaths } from './mount'

export function kea (input) {
  const globalPlugins = getCache().plugins
  const plugins = input.plugins && input.plugins.length > 0 ? [...globalPlugins, ...input.plugins] : [...globalPlugins]

  plugins.forEach(p => p.beforeKea && p.beforeKea(input))

  const mountDirectly = !input.key && !hasConnectWithKey(input.connect)
  const lazy = (input.options && input.options.lazy) || !mountDirectly || false

  const wrapper = (Klass) => {
    // make this.actions work if it's a React.Component we're operating with
    injectActionsIntoClass(Klass)

    const Connect = reduxConnect(
      mapStateToPropsCreator(input, plugins),
      mapDispatchToPropsCreator(input, plugins)
    )(Klass)

    // inject proptypes into the class if it's a React.Component
    // not using useRef here since we do it only once per component
    let injectPropTypes = !isStateless(Klass)

    return function Kea (props) {
      const logic = convertInputToLogic({ input, props, plugins })

      // inject proptypes to React.Component
      if (injectPropTypes && logic.propTypes) {
        injectPropTypes = false
        Klass.propTypes = Object.assign(Klass.propTypes || {}, logic.propTypes)
      }

      // mount paths only on first render
      const firstRender = useRef(true)
      if (firstRender.current) {
        firstRender.current = false

        // give access to the logic to the return value
        if (!mountDirectly) {
          Object.assign(wrapper, logic)
        }

        mountPaths(logic, plugins)
      }

      // unmount paths when component gets removed
      useEffect(() => () => unmountPaths(logic, plugins, lazy), [])

      // TODO: unmount & remount if path changed

      plugins.forEach(p => p.beforeRender && p.beforeRender(logic, props))

      return <Connect {...props} />
    }
  }

  // TODO: legacy names. remove/change them?
  wrapper._isKeaFunction = true
  wrapper._isKeaSingleton = mountDirectly

  if (input.key) {
    wrapper.withKey = keyOrCreator => {
      if (typeof keyOrCreator === 'function') {
        const withKey = props => {
          const logic = convertInputToLogic({ input, key: keyOrCreator(props), props, plugins })
          return Object.assign({}, wrapper, logic)
        }
        withKey._isKeaWithKey = true
        return withKey
      } else {
        const logic = convertInputToLogic({ input, key: keyOrCreator, plugins })
        return Object.assign({}, wrapper, logic)
      }
    }
  }

  if (mountDirectly) {
    const logic = convertInputToLogic({ input, plugins })

    // if we're in eager mode (!lazy), attach the reducer directly
    if (!lazy && logic.reducer && !logic.mounted) {
      attachReducer(logic.path, logic.reducer)
      logic.mounted = true
    }

    Object.assign(wrapper, logic)
  } else {
    Object.assign(wrapper, convertPartialDynamicInput({ input, plugins }))
  }

  return wrapper
}

export function connect (input) {
  return kea({ connect: input })
}

const mapStateToPropsCreator = (input, plugins) => (state, ownProps) => {
  const logic = convertInputToLogic({ input, props: ownProps, plugins })

  let resp = {}
  Object.entries(logic.selectors).forEach(([key, selector]) => {
    resp[key] = selector(state, ownProps)
  })

  return resp
}

const mapDispatchToPropsCreator = (input, plugins) => (dispatch, ownProps) => {
  const logic = convertInputToLogic({ input, props: ownProps, plugins })

  let actions = Object.assign({}, ownProps.actions)

  Object.entries(logic.actions).forEach(([key, action]) => {
    actions[key] = (...args) => dispatch(action(...args))
  })

  return {
    dispatch: dispatch,
    actions: actions
  }
}

function isStateless (Component) {
  return (
    typeof Component === 'function' && // can be various things
    !(Component.prototype && Component.prototype.isReactComponent) // native arrows don't have prototypes // special property
  )
}

// inject to the component something that converts this.props.actions to this.actions
function injectActionsIntoClass (Klass) {
  if (!isStateless(Klass)) {
    if (!Object.getOwnPropertyDescriptor(Klass.prototype, 'actions')) {
      Object.defineProperty(Klass.prototype, 'actions', {
        get: function actions () {
          return this.props.actions
        }
      })
    }
  }
}

@@ -627,80 +627,8 @@
 ))%0A%0A
-  const mountDirectly = !input.key && !hasConnectWithKey(input.connect)%0A
   co
@@ -680,29 +680,62 @@
 zy) %7C%7C !
-mountDirectly
+!input.key %7C%7C hasConnectWithKey(input.connect)
  %7C%7C fals
@@ -1741,21 +1741,11 @@
 if (
-!mountDirectl
+laz
 y) %7B
@@ -2238,28 +2238,20 @@
 leton = 
-mountDirectl
+!laz
 y%0A%0A  if 
@@ -2779,20 +2779,12 @@
 if (
-mountDirectl
+!laz
 y) %7B

redefine what it means to be lazy