import { Context } from './context'
import { combine } from 'regl-combine'
import extend from 'extend'

/**
 * Creates a function with initial (default) state that is given
 * to optional components when invoked as default argument state.
 *
 * Entity(ctx, initialState, ...components) -> (args, scope) -> Any
 * Entity(ctx, initialState, [...components]) -> (args, scope) -> Any
 * Entity(ctx, ...components) -> (args, scope) -> Any
 * Entity(ctx, [...components]) -> (args, scope) -> Any
 *
 * @public
 * @function
 * @param {Context} ctx
 * @param {Object} initialState
 * @return {Function}
 */
export function Entity(ctx, initialState, ...components) {
  if (!ctx || 'object' != typeof ctx || Array.isArray(ctx)) {
    throw new TypeError("Entity(): Expecting context object.")
  }

  if (initialState) {
    if (Array.isArray(initialState)) {
      components = initialState
      initialState = {}
    } else if ('function' == typeof initialState) {
      components.unshift(initialState)
    } else if ('object' != typeof initialState) {
      throw new TypeError("Entity(): Expecting initial state to be an object.")
    }
  }

  if (Array.isArray(components[0])) {
    components = components[0]
  }

  initialState = { ...initialState }

  const entityId = generateEntityId()
  const flatComponents = flattenComponents(initialState, components)
  const combinedComponents = combine(ctx.regl, [
    ...flatComponents.components,
    {context: {entityId}},
  ])

  return Object.assign((...args) => {
    return combinedComponents(...parseArguments(initialState, ...args))
  }, { initialState, components, combinedComponents, entityId })
}

/**
 * noop(void) -> void
 */
const noop = () => void 0

/**
 * Generates an unique entity id.
 *
 * generateEntityId() -> Number
 */
let entityCount = 0
function generateEntityId() {
  return ++ entityCount
}

/**
 * Flatten and filter compoments into a single array.
 *
 * flattenComponents(components: [...Function|[Function]) -> [...Function]
 */
function flattenComponents(initialState = {}, ...components){
  components = Array.isArray(components[0]) ? components[0] : components
  return {
    initialState,
    components: components.filter(filter).map(map).reduce(reduce, [])
  }

  function filter(component) {
    return 'function' == typeof component
  }

  function map(component) {
    if ('entityId' in component && Array.isArray(component.components)) {
      if (component.initialState && 'object' == typeof component.initialState) {
        extend(true, initialState, component.initialState)
      }
      return flattenComponents(initialState, component.components).components
    } else {
      return component
    }
  }

  function reduce(components, component) {
    return components.concat(component)
  }
}

/**
 * Normalizes entity invokation arguments.
 * parseArguments(args, scope) -> [Any, Function]
 */
function parseArguments(defaults, args, scope) {
  if (defaults && 'object' == typeof defaults) {
    defaults = Object.assign({}, defaults, args)
  }
  if ('function' == typeof args) {
    if ('function' == typeof scope) {
      args = args()
    } else {
      scope = args;
      args = {}
    }
  } else if (args) {
    if (Array.isArray(args)) {
      args = Object.assign([], args)
    } else if ('object' == typeof args) {
      args = Object.assign({}, args)
    }
  }
  if (null == args) { args = {} }
  if (null == scope) { scope = noop }
  if (args && 'object' == typeof args && !Array.isArray(args)) {
    args = Object.assign({}, args, defaults)
  }
  return [args, scope]
}

@@ -1,16 +1,82 @@
+import %7B MissingContextError, BadArgumentError %7D from '../errors'%0A
 import %7B Context
@@ -654,16 +654,75 @@
 nction%7D%0A
+ * @throws MissingContextError%0A * @throws BadArgumentError%0A
  */%0Aexpo
@@ -782,17 +782,103 @@
 %7B%0A  if (
-!
+undefined === ctx) %7B%0A    throw new MissingContextError('Entity')%0A  %7D else if (null === 
 ctx %7C%7C '
@@ -942,55 +942,48 @@
 new 
-TypeError(%22Entity(): Expecting context 
+BadArgumentError(0, 'ctx', ctx, '
 object
-.%22
+'
 )%0A  
@@ -1263,46 +1263,51 @@
 new 
-TypeError(%22Entity(): Expecting
+BadArgumentError(1, 'initialState',
  initial
  sta
@@ -1306,32 +1306,23 @@
 tial
- s
+S
 tate
- to be an 
+, '
 object
-.%22
+'
 )%0A  
@@ -1480,16 +1480,89 @@
 ityId()%0A
+  const entityContext = ctx.regl(%7Bcontext: new EntityContext(entityId)%7D)%0A
   const 
@@ -1673,17 +1673,8 @@
 gl, 
-%5B%0A    ...
 flat
@@ -1698,81 +1698,174 @@
 ents
-,%0A    %7Bcontext: %7BentityId%7D%7D,%0A  %5D)%0A%0A
+)%0A%0A  return Object.assign((...vargs) =%3E %7B%0A    const %5Bkargs, next%5D = parseArguments(initialState, ...vargs)%0A 
   
+ 
 return 
-Object.assign((...args
+entityContext(kargs, (%7B%7D, args, batchId
 ) =%3E %7B%0A
+  
     
@@ -1894,56 +1894,123 @@
 nts(
-...parseArguments(initialState, ...args))%0A  %7D, %7B
+args, (ctx, cargs) =%3E %7B%0A        return next(ctx, cargs, batchId)%0A      %7D)%0A    %7D)%0A  %7D, %7B%0A    combinedComponents,%0A   
  ini
@@ -2011,32 +2011,36 @@
    initialState,
+%0A   
  components, com
@@ -2039,40 +2039,261 @@
 nts,
- combinedComponents, entityId %7D)
+%0A    entityId,%0A  %7D)%0A%7D%0A%0A/**%0A * Entity context object wrapper.%0A */%0Aclass EntityContext %7B%0A  constructor(entityId) %7B%0A    Object.assign(this, %7B%0A      entityId() %7B return entityId %7D,%0A      batchId(%7B%7D, args, batchId) %7B%0A        return batchId%0A      %7D%0A    %7D)%0A  %7D
 %0A%7D%0A%0A

Enhance Entity