import React from "react"
import { createStore, applyMiddleware, bindActionCreators, compose } from "redux"
import Im, { fromJS, Map } from "immutable"
import deepExtend from "deep-extend"
import { combineReducers } from "redux-immutable"
import { serializeError } from "serialize-error"
import merge from "lodash/merge"
import { NEW_THROWN_ERR } from "core/plugins/err/actions"
import win from "core/window"

import { systemThunkMiddleware, isFn, objMap, objReduce, isObject, isArray, isFunc } from "core/utils"

const idFn = a => a

// Apply middleware that gets sandwitched between `dispatch` and the reducer function(s)
function createStoreWithMiddleware(rootReducer, initialState, getSystem) {

  let middlwares = [
    // createLogger( {
    //   stateTransformer: state => state && state.toJS()
    // } ),
    systemThunkMiddleware( getSystem )
  ]

  const composeEnhancers = win.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

  return createStore(rootReducer, initialState, composeEnhancers(
    applyMiddleware( ...middlwares )
  ))
}

export default class Store {

  constructor(opts={}) {
    deepExtend(this, {
      state: {},
      plugins: [],
      system: {
        configs: {},
        fn: {},
        components: {},
        rootInjects: {},
        statePlugins: {}
      },
      boundSystem: {},
      toolbox: {}
    }, opts)

    this.getSystem = this._getSystem.bind(this)

    // Bare system (nothing in it, besides the state)
    this.store = configureStore(idFn, fromJS(this.state), this.getSystem )

    // will be the system + Im, we can add more tools when we need to
    this.buildSystem(false)

    // Bootstrap plugins
    this.register(this.plugins)
  }

  getStore() {
    return this.store
  }

  register(plugins, rebuild=true) {
    var pluginSystem = combinePlugins(plugins, this.getSystem())
    systemExtend(this.system, pluginSystem)
    if(rebuild) {
      this.buildSystem()
    }

    const needAnotherRebuild = callAfterLoad.call(this.system, plugins, this.getSystem())

    if(needAnotherRebuild) {
      this.buildSystem()
    }
  }

  buildSystem(buildReducer=true) {
    let dispatch = this.getStore().dispatch
    let getState = this.getStore().getState

    this.boundSystem = Object.assign({},
        this.getRootInjects(),
        this.getWrappedAndBoundActions(dispatch),
        this.getWrappedAndBoundSelectors(getState, this.getSystem),
        this.getStateThunks(getState),
        this.getFn(),
        this.getConfigs()
     )

    if(buildReducer)
      this.rebuildReducer()
  }

  _getSystem() {
    return this.boundSystem
  }

  getRootInjects() {
    return Object.assign({
      getSystem: this.getSystem,
      getStore: this.getStore.bind(this),
      getComponents: this.getComponents.bind(this),
      getState: this.getStore().getState,
      getConfigs: this._getConfigs.bind(this),
      Im,
      React
    }, this.system.rootInjects || {})
  }

  _getConfigs(){
    return this.system.configs
  }

  getConfigs() {
    return {
      configs: this.system.configs
    }
  }

  setConfigs(configs) {
    this.system.configs = configs
  }

  rebuildReducer() {
    this.store.replaceReducer(buildReducer(this.system.statePlugins, this.getSystem))
  }

  /**
   * Generic getter from system.statePlugins
   *
   */
  getType(name) {
    let upName = name[0].toUpperCase() + name.slice(1)
    return objReduce(this.system.statePlugins, (val, namespace) => {
        let thing = val[name]
        if(thing)
        return {[namespace+upName]:  thing}
      })
  }

  getSelectors() {
    return this.getType("selectors")
  }

  getActions() {
    let actionHolders = this.getType("actions")

    return objMap(actionHolders, (actions) => {
      return objReduce(actions, (action, actionName) => {
        if(isFn(action))
          return {[actionName]: action}
      })
    })
  }

  getWrappedAndBoundActions(dispatch) {
    let actionGroups = this.getBoundActions(dispatch)
      return objMap(actionGroups, (actions, actionGroupName) => {
        let wrappers = this.system.statePlugins[actionGroupName.slice(0,-7)].wrapActions
          if(wrappers) {
            return objMap(actions, (action, actionName) => {
              let wrap = wrappers[actionName]
              if(!wrap) {
                return action
              }

              if(!Array.isArray(wrap)) {
                wrap = [wrap]
              }
              return wrap.reduce((acc, fn) => {
                let newAction = (...args) => {
                  return fn(acc, this.getSystem())(...args)
                }
                if(!isFn(newAction)) {
                  throw new TypeError("wrapActions needs to return a function that returns a new function (ie the wrapped action)")
                }
                return wrapWithTryCatch(newAction, this.getSystem)
              }, action || Function.prototype)
            })
          }
        return actions
      })
  }

  getWrappedAndBoundSelectors(getState, getSystem) {
    let selectorGroups = this.getBoundSelectors(getState, getSystem)
      return objMap(selectorGroups, (selectors, selectorGroupName) => {
        let stateName = [selectorGroupName.slice(0, -9)] // selectors = 9 chars
        let wrappers = this.system.statePlugins[stateName].wrapSelectors
          if(wrappers) {
            return objMap(selectors, (selector, selectorName) => {
              let wrap = wrappers[selectorName]
              if(!wrap) {
                return selector
              }

              if(!Array.isArray(wrap)) {
                wrap = [wrap]
              }
              return wrap.reduce((acc, fn) => {
                let wrappedSelector = (...args) => {
                  return fn(acc, this.getSystem())(getState().getIn(stateName), ...args)
                }
                if(!isFn(wrappedSelector)) {
                  throw new TypeError("wrapSelector needs to return a function that returns a new function (ie the wrapped action)")
                }
                return wrappedSelector
              }, selector || Function.prototype)
            })
          }
        return selectors
      })
  }

  getStates(state) {
    return Object.keys(this.system.statePlugins).reduce((obj, key) => {
      obj[key] = state.get(key)
      return obj
    }, {})
  }

  getStateThunks(getState) {
    return Object.keys(this.system.statePlugins).reduce((obj, key) => {
        obj[key] = ()=> getState().get(key)
    return obj
  }, {})
  }

  getFn() {
    return {
      fn: this.system.fn
    }
  }

  getComponents(component) {
    const res = this.system.components[component]

    if(Array.isArray(res)) {
      return res.reduce((ori, wrapper) => {
        return wrapper(ori, this.getSystem())
      })
    }
    if(typeof component !== "undefined") {
      return this.system.components[component]
    }

    return this.system.components
  }

  getBoundSelectors(getState, getSystem) {
    return objMap(this.getSelectors(), (obj, key) => {
      let stateName = [key.slice(0, -9)] // selectors = 9 chars
      const getNestedState = ()=> getState().getIn(stateName)

      return objMap(obj, (fn) => {
        return (...args) => {
          let res = wrapWithTryCatch(fn, this.getSystem).apply(null, [getNestedState(), ...args])

          //  If a selector returns a function, give it the system - for advanced usage
          if(typeof(res) === "function")
            res = wrapWithTryCatch(res, this.getSystem)(getSystem())

          return res
        }
      })
    })
  }

  getBoundActions(dispatch) {

    dispatch = dispatch || this.getStore().dispatch

    const actions = this.getActions()

    const process = creator =>{
      if( typeof( creator ) !== "function" ) {
        return objMap(creator, prop => process(prop))
      }

      return ( ...args )=>{
        var action = null
        try{
          action = creator( ...args )
        }
        catch( e ){
          action = {type: NEW_THROWN_ERR, error: true, payload: serializeError(e) }
        }
        finally{
          return action // eslint-disable-line no-unsafe-finally
        }
      }

    }
    return objMap(actions, actionCreator => bindActionCreators( process( actionCreator ), dispatch ) )
  }

  getMapStateToProps() {
    return () => {
      return Object.assign({}, this.getSystem())
    }
  }

  getMapDispatchToProps(extras) {
    return (dispatch) => {
      return deepExtend({}, this.getWrappedAndBoundActions(dispatch), this.getFn(), extras)
    }
  }

}

function combinePlugins(plugins, toolbox) {
  if(isObject(plugins) && !isArray(plugins)) {
    return merge({}, plugins)
  }

  if(isFunc(plugins)) {
    return combinePlugins(plugins(toolbox), toolbox)
  }

  if(isArray(plugins)) {
    return plugins
      .map(plugin => combinePlugins(plugin, toolbox))
      .reduce(systemExtend, { components: toolbox.getComponents() })
  }

  return {}
}

function callAfterLoad(plugins, system, { hasLoaded } = {}) {
  let calledSomething = hasLoaded
  if(isObject(plugins) && !isArray(plugins)) {
    if(typeof plugins.afterLoad === "function") {
      calledSomething = true
      wrapWithTryCatch(plugins.afterLoad, system.getSystem).call(this, system)
    }
  }

  if(isFunc(plugins))
    return callAfterLoad.call(this, plugins(system), system, { hasLoaded: calledSomething })

  if(isArray(plugins)) {
    return plugins.map(plugin => callAfterLoad.call(this, plugin, system, { hasLoaded: calledSomething }))
  }

  return calledSomething
}

// Wraps deepExtend, to account for certain fields, being wrappers.
// Ie: we need to convert some fields into arrays, and append to them.
// Rather than overwrite
function systemExtend(dest={}, src={}) {

  if(!isObject(dest)) {
    return {}
  }
  if(!isObject(src)) {
    return dest
  }

  // Wrap components
  // Parses existing components in the system, and prepares them for wrapping via getComponents
  if(src.wrapComponents) {
    objMap(src.wrapComponents, (wrapperFn, key) => {
      const ori = dest.components && dest.components[key]
      if(ori && Array.isArray(ori)) {
        dest.components[key] = ori.concat([wrapperFn])
        delete src.wrapComponents[key]
      } else if(ori) {
        dest.components[key] = [ori, wrapperFn]
        delete src.wrapComponents[key]
      }
    })

    if(!Object.keys(src.wrapComponents).length) {
      // only delete wrapComponents if we've matched all of our wrappers to components
      // this handles cases where the component to wrap may be out of our scope,
      // but a higher recursive `combinePlugins` call will be able to handle it.
      delete src.wrapComponents
    }
  }


  // Account for wrapActions, make it an array and append to it
  // Modifies `src`
  // 80% of this code is just safe traversal. We need to address that ( ie: use a lib )
  const { statePlugins } = dest
  if(isObject(statePlugins)) {
    for(let namespace in statePlugins) {
      const namespaceObj = statePlugins[namespace]
      if(!isObject(namespaceObj)) {
        continue
      }

      const { wrapActions, wrapSelectors } = namespaceObj

      // process action wrapping
      if (isObject(wrapActions)) {
        for(let actionName in wrapActions) {
          let action = wrapActions[actionName]

          // This should only happen if dest is the first plugin, since invocations after that will ensure its an array
          if(!Array.isArray(action)) {
            action = [action]
            wrapActions[actionName] = action // Put the value inside an array
          }

          if(src && src.statePlugins && src.statePlugins[namespace] && src.statePlugins[namespace].wrapActions && src.statePlugins[namespace].wrapActions[actionName]) {
            src.statePlugins[namespace].wrapActions[actionName] = wrapActions[actionName].concat(src.statePlugins[namespace].wrapActions[actionName])
          }

        }
      }

      // process selector wrapping
      if (isObject(wrapSelectors)) {
        for(let selectorName in wrapSelectors) {
          let selector = wrapSelectors[selectorName]

          // This should only happen if dest is the first plugin, since invocations after that will ensure its an array
          if(!Array.isArray(selector)) {
            selector = [selector]
            wrapSelectors[selectorName] = selector // Put the value inside an array
          }

          if(src && src.statePlugins && src.statePlugins[namespace] && src.statePlugins[namespace].wrapSelectors && src.statePlugins[namespace].wrapSelectors[selectorName]) {
            src.statePlugins[namespace].wrapSelectors[selectorName] = wrapSelectors[selectorName].concat(src.statePlugins[namespace].wrapSelectors[selectorName])
          }

        }
      }
    }
  }

  return deepExtend(dest, src)
}

function buildReducer(states, getSystem) {
  let reducerObj = objMap(states, (val) => {
    return val.reducers
  })
  return allReducers(reducerObj, getSystem)
}

function allReducers(reducerSystem, getSystem) {
  let reducers = Object.keys(reducerSystem).reduce((obj, key) => {
    obj[key] = makeReducer(reducerSystem[key], getSystem)
    return obj
  },{})

  if(!Object.keys(reducers).length) {
    return idFn
  }

  return combineReducers(reducers)
}

function makeReducer(reducerObj, getSystem) {
  return (state = new Map(), action) => {
    if(!reducerObj)
      return state

    let redFn = (reducerObj[action.type])
    if(redFn) {
      const res = wrapWithTryCatch(redFn, getSystem)(state, action)
      // If the try/catch wrapper kicks in, we'll get null back...
      // in that case, we want to avoid making any changes to state
      return res === null ? state : res
    }
    return state
  }
}

function wrapWithTryCatch(fn, getSystem, {
  logErrors = true
} = {}) {
  if(typeof fn !== "function") {
    return fn
  }

  return function(...args) {
    try {
      return fn.call(this, ...args)
    } catch(error) {
      if(logErrors) {
        const { uncaughtExceptionHandler} = getSystem().getConfigs()
        if (typeof uncaughtExceptionHandler === "function") {
          uncaughtExceptionHandler(error)
        } else {
          console.error(error)
        }
      }
      return null
    }
  }
}

function configureStore(rootReducer, initialState, getSystem) {
  const store = createStoreWithMiddleware(rootReducer, initialState, getSystem)

  // if (module.hot) {
  //   // Enable Webpack hot module replacement for reducers
  //   module.hot.accept("reducers/index", () => {
  //     const nextRootReducer = require("reducers/index")
  //     store.replaceReducer(nextRootReducer)
  //   })
  // }

  return store
}
