always = require('ramda/es/always').default; curry = require('ramda/es/curry').default; find = require('ramda/es/find').default; flip = require('ramda/es/flip').default; has = require('ramda/es/has').default; invoker = require('ramda/es/invoker').default; isEmpty = require('ramda/es/isEmpty').default; map = require('ramda/es/map').default; match = require('ramda/es/match').default; merge = require('ramda/es/merge').default; partition = require('ramda/es/partition').default; pick = require('ramda/es/pick').default; reject = require('ramda/es/reject').default; replace = require('ramda/es/replace').default; type = require('ramda/es/type').default; values = require('ramda/es/values').default; whereEq = require('ramda/es/whereEq').default; without = require('ramda/es/without').default; #auto_require: srcramda
{change, reduceO, isAffected, func, $, isNilOrEmpty, sf0, customError} = RE = require 'ramda-extras' #auto_require: ramda-extras
[] = [] #auto_sugar
qq = (f) -> console.log match(/return (.*);/, f.toString())[1], f()
qqq = (f) -> console.log match(/return (.*);/, f.toString())[1], JSON.stringify(f(), null, 2)
_ = (...xs) -> xs

# TODO: Move parseArguments out of popsiql
popsiql = require 'popsiql'

PE = customError 'PhloxError'

module.exports = func
	ui: Object
	queries: Object
	lifters: Object
	invokers: Object
	config:
		runQuery: ->
		runLifter: ->
		runInvoker: ->
		debug: Boolean
		report: () -> # report callback for logging 
,
({ui, queries, lifters, invokers, config}) ->
	return new Phlox {ui, queries, lifters, invokers, config}


class Phlox
	constructor: ({ui, queries, lifters, invokers, config}) ->
		@ui = ui
		@state = {}
		@data = {}

		[qli, @noDepQueries, @noDepInvokers] = _prepare {ui, queries, lifters, invokers}
		[i, ql] = partition whereEq({type: 'invoker'}), qli
		@queriesAndLifters = ql
		@invokers = i
		@listeners = []
		# TODO: try resolve q/l/i into listeners !!

		window.setTimeout (=> @_runNoDepQueriesAndLifters()), 50

		# @initialUI = ui
		# @initialData = data
		@uiChanges = ui # initial ui is the initial change to spark everything off
		@dataChanges = {}
		@isRunning = false

		# @commitHistory = [] # optimization: cap this to X items in a smart way to keep index consistent

		@config = config

		@setCount = 0
		@flushCount = 0
		@isBlocked = false

		# @_flush() # initial flush
		window.requestAnimationFrame @_flush



	setUI: curry (delta, forceFlush=false) ->
		@setCount = @setCount + 1
		undo = {}
		@ui = change.meta delta, @ui, undo, @uiChanges
		if forceFlush then @_flush()
		# optimization 0: use changeM instead, reasoning: if views for some reason rerenders before flush, they'll partially get some new data, no problem with that?
		# optimization 1: call data-only dependencies before flush (requires rethink of viewModels)
		# optimization 2: move flush to WebWorker

	sub: (deps, cb, name = undefined, commitId = undefined) ->
		listener = if name then {deps, cb, name} else {deps, cb}
		@listeners.push listener

		# if ! isNil commitId
		# 	for id in [commitId..@commitHistory.length]
		# 		qq => _ name, id, isAffected deps, @commitHistory[id]
		# 		if isAffected deps, @commitHistory[id]
		# 			cb {UI: @ui, Data: @data, State: @state}
		# 			break

		# cb {UI: @ui, Data: @data, State: @state} # call first with current data
		# initialData = {UI: @ui, Data: @data, State: @state}
		return () => @listeners = without [listener], @listeners

		# unsub = () => @listeners = without [listener], @listeners
		# return [initialData, unsub]

	# getUDS: () -> return [{UI: @ui, Data: @data, State: @state}, @commitHistory.length]
	getUDS: () -> return {UI: @ui, Data: @data, State: @state}

	block: () -> @isBlocked = true
	unblock: () -> @isBlocked = false

	reset: (ui) ->
		# NOTE: not sure if this is a correct = full reset

		if !ui then throw new PE "initial ui needs to be an object, not #{ui}"
		# reset data
		for k,d of @data
			@_setData k, undefined

		# figure out the total delta needed for the reset
		totalDelta = {}
		for k, v of @ui
			if ui[k] then totalDelta[k] = ui[k]
			else totalDelta[k] = undefined

		@setUI totalDelta

		# re-run queries and lifters without dependencies
		window.setTimeout (=>@_runNoDepQueriesAndLifters()), 50




	_setData: (key) -> curry (data, forceFlush=false) =>
		@setCount = @setCount + 1
		undo = {}
		delta = {[key]: always data}
		@data = change.meta delta, @data, undo, @dataChanges
		if forceFlush then @_flush()

	_flush: =>
		if @isBlocked then return window.requestAnimationFrame @_flush
		if @flushCount > 0 && isEmpty(@uiChanges) && isEmpty(@dataChanges) then return window.requestAnimationFrame @_flush
		# if @flushCount > 0 && isEmpty(@uiChanges) && isEmpty(@dataChanges) then return

		# RUN
		setCount = @setCount
		# if flushCount == 0
		# 	dataChangesBefore = @initialData
		# else
		dataChangesBefore = @dataChanges
		uiChanges = @uiChanges

		@config.report {ts: performance.now(), name: 'flush-start', uiChanges, dataChangesBefore, setCount}

		r0 = performance.now()
		@setCount = 0

		time = {}
		ui = @ui
		@uiChanges = {} # reset so new uiChanges theoretically can happen during the run

		dataBefore = @data
		@dataChanges = {} # reset so new dataChanges theoretically can happen during the run

		ql0 = performance.now()
		[data, dataChanges, state, stateChanges, affected] = @_runQueriesAndLifters ui, uiChanges, dataBefore, dataChangesBefore
		time.ql = performance.now() - ql0

		@data = change dataChanges, @data
		@state = state

		time.r = performance.now() - r0
		# @config.report {ts: r0, name: 'run', uiChanges, dataChanges, stateChanges, setCount, time}

		# if flushCount == 0
		# 	@commitHistory.push {UI: uiChanges, Data: dataChanges, State: stateChanges}
		# else @commitHistory.push {UI: uiChanges, Data: dataChanges, State: stateChanges}

		i0 = performance.now()
		affectedInvokers = @_runInvokers ui, data, state, uiChanges, dataChanges, stateChanges
		time.i = performance.now() - i0

		# LISTENERS
		l0 = performance.now()
		affectedListeners = @_runListeners ui, data, state, uiChanges, dataChanges, stateChanges
		time.lis = performance.now() - l0
		time.tot = performance.now() - r0

		@config.report {ts: r0, name: 'flush-end', uiChanges, dataChangesBefore, dataChanges, stateChanges,
		setCount, time, affected: merge affected, {invokers: affectedInvokers, listeners: affectedListeners}}

		@flushCount++

		window.requestAnimationFrame @_flush


	_runQueriesAndLifters: (ui, uiChanges, dataBefore, dataChangesBefore) ->
		dataChanges = dataChangesBefore
		stateChanges = {}
		data = dataBefore
		state = @state
		affected = {queries: [], lifters: []}
		for x in @queriesAndLifters
			# qq -> x
			# qq -> isAffected x.deps, {UI: uiChanges, Data: dataChanges, State: stateChanges}
			if isAffected x.deps, {UI: uiChanges, Data: dataChanges, State: stateChanges}
				if x.type == 'query'
					affected.queries.push x.key
					clientRes = @config.runQuery x, {UI: ui, Data: data, State: state}, @_setData x.key
					if clientRes != undefined
						data = change.meta {[x.key]: clientRes}, data, {}, dataChanges
				else
					affected.lifters.push x.key
					lifterRes = @config.runLifter x, {UI: ui, Data: data, State: state}
					if lifterRes != undefined
						state = change.meta {[x.key]: lifterRes}, state, {}, stateChanges

		return [data, dataChanges, state, stateChanges, affected]

	_runInvokers: (ui, data, state, uiChanges, dataChanges, stateChanges) ->
		affected = []
		for i in @invokers
			if isAffected i.deps, {UI: uiChanges, Data: dataChanges, State: stateChanges}
				@config.runInvoker i, {UI: ui, Data: data, State: state}
				affected.push i
		return affected

	_runListeners: (ui, data, state, uiChanges, dataChanges, stateChanges) ->
		state = @state
		affected = []
		for l in @listeners
			if isAffected l.deps, {UI: uiChanges, Data: dataChanges, State: stateChanges}
				l.cb {UI: ui, Data: data, State: state}
				affected.push l
		return affected

	_runNoDepQueriesAndLifters: () ->
		for q in @noDepQueries # run queries without dependencies
			# optimization: do this async instead to improve time to first paint
			clientRes = @config.runQuery q, {UI: {}, Data: {}, State: {}}, @_setData q.key
			if clientRes != undefined
				@_setData q.key, clientRes

		for i in @noDepInvokers # run invokers without dependencies
			# optimization: do this async instead to improve time to first paint
			@config.runInvoker i, {UI: {}, Data: {}, State: {}}





###### Utils

_areResolved = (deps, resMap, level = 0) ->
	if level >= 2 then return true # {UI: {a: {a1}}} <-- we resolve to level a, not a1

	for k,v of deps
		if ! has k, resMap then return false
		else if v != null && ! _areResolved v, resMap[k], level + 1 then return false

	return true


_prepare = ({ui, queries, lifters, invokers}) ->
	toResolve = {}
	noDepQueries = []
	noDepInvokers = []

	# remove debug from ui
	ui = $ ui, flip(reduceO) {}, (acc, v, k) -> merge acc, {[replace /_debug$/, '', k]: v}

	_toQLI = (type, k, f) ->
		key = replace /_debug$/, '', k
		[UI, Data, State] = popsiql.utils.parseArguments f.toString()
		if has k, ui then throw new PE "#{type} '#{key}' also exists in initial ui, pick a unique key"
		if has(k, toResolve) || find(whereEq({key}), noDepQueries)
			throw new PE "'#{key}' exists twice in queries/lifters/invokers"
		qli = {type, key, f, debug: key != k, deps: reject isNilOrEmpty, {UI, Data, State}}
		if isEmpty qli.deps
			if type == 'lifter' then throw new PE "#{type}/#{k} is missing dependencies"
		return qli

	resMap = {UI: ui, Data: {}, State: {}}

	for k,f of queries
		qli = _toQLI 'query', k, f
		if isEmpty qli.deps
			noDepQueries.push qli
			resMap.Data[qli.key] = 1
		else toResolve[qli.key] = qli
	for k,f of lifters
		qli = _toQLI 'lifter', k, f
		toResolve[qli.key] = qli
	for k,f of invokers
		qli = _toQLI 'invoker', k, f
		if isEmpty qli.deps
			noDepInvokers.push qli
		else toResolve[qli.key] = qli

	res = []

	lap = 0
	while !isEmpty toResolve

		toDelete = []
		for k,o of toResolve
			if ! _areResolved o.deps, resMap then continue

			toDelete.push k

			if o.type == 'query'
				res.push o
				resMap.Data[o.key] = 1
			else if o.type == 'lifter'
				res.push o
				resMap.State[o.key] = 1
			else if o.type == 'invoker'
				res.push o

		for d in toDelete
			delete toResolve[d]

		if lap++ > 20
			console.error toResolve
			throw new PE "cannot resolve: #{sf0 values $ toResolve, map ({type, key}) -> type+'/'+key}"

	return [res, noDepQueries, noDepInvokers]

module.exports._prepare = _prepare

