# src/listeners/Logger.js

    import uuid from "uuid";
    import autobind from "autobind-decorator";
    import has from "lodash.has";
    import invariant from "invariant";
    import isFunction from "lodash.isfunction";
    import isEqual from "lodash.isequal";
    import isString from "lodash.isstring"
    import sortBy from "lodash.sortby";
    import result from "lodash.result";

    const HelpMsg = `f.lux logger commands:
    \tback          - moves backward in time by one store state frame
    \tclear         - removes all logs
    \tforward       - moves forward in time by one store state frame
    \thelp          - f.lux logger commands
    \tindex         - active index of store state frames
    \tmaxFrames     - # of store updates to cache (default=50)
    \nnext          - alias for forward
    \tprint         - print logs to console
    \tprintNoState  - print logs to console without state objects
    \tsize          - # of store state frames available
    \tstore         - gets the f.lux store
    \nFunctions:
    \tclearTrap(name)                    - clears a trap set by 'setTrap()'
    \tgoto(idx)                          - move to a specific store state frame
    \tsetMaxFrames(maxFrames)            - set the maximum number of store states to maintain (default=50)
    \tsetTrap(cond, value, name=uuid)    - sets a debugger trap and returns name. condition argument may be
    \t                                     a function taking next state or a string path to get a value
    \ttail(count=10, printState=true)    - prints last 'count' store updates
    \n
    f.lux log available at window.`;

    export default class Logger {
    	constructor(store, name="flog") {
    		this.store = store;
    		this.name = name;
    		this.nextFrameId = 1;
    		this.filter = null;
    		this.frames = [];
    		this.maxFrames = 50;
    		this.traps = null;

    		this.activeFrame = new LogFrame(store, this.nextFrameId++);

    		this.activeFrame.captureState();
    		this.frames.push(this.activeFrame);

    		this.currFrame = new LogFrame(store, this.nextFrameId++);

    		window[name] = this.console = createConsoleLogger(this);

    		// print help message to console
    		this.console.help;
    	}

    	clear() {
    		this.activeFrame = new LogFrame(this.store, this.nextFrameId++);
    		this.activeFrame.captureState();

    		this.frames = [ this.activeFrame ];
    		this.currFrame = new LogFrame(this.store, this.nextFrameId++);
    	}

    	clearTrap(name) {
    		if (!this.traps) { return }

    		delete this.traps[name];

    		if (Object.keys(this.traps).length === 0) {
    			this.traps = null;
    		}
    	}

    	print(printState=true) {
    		const frames = this.frames;

    		console.log(`f.lux log`);

    		for (let i=0, frame; frame=frames[i]; i++) {
    			frame.print(this.filter, frames[i-1], printState);
    		}

    		if (this.currFrame.actions.length) {
    			console.log(`\nf.lux log current frame:`);
    			this.currFrame.print(null, null, printState);
    		}

    		console.log("\n\n");
    	}

    	setActionFilter(filter) {
    		if (filter instanceof RegExp) {
    			this.filter = af => filter.test(`${af.impl.dotPath()}::${af.action.name}`);
    		} else {
    			this.filter = filter;
    		}
    	}

    	setMaxFrames(maxFrames) {
    		this.maxFrames = maxFrames;

    		this.truncateFrames();
    	}

    	setTrap(condition, value, name=uuid()) {
    		invariant(isFunction(condition) || isString(condition), "Traps must be either a regular expression or function");

    		this.traps = this.traps || {};

    		this.traps[name] = {
    			eval: condition,
    			name: name,
    			value: value
    		}

    		return name;
    	}

    	tail(count=10, printState=true) {
    		const frames = this.frames;
    		const start = frames.length>count ?frames.length - count :0;
    		var prevFrame;

    		console.log(`f.lux tail - count=${count}, printState=${printState}`);

    		for (let i=start, frame; frame=this.frames[i]; i++) {
    			frame.print(this.filter, prevFrame, printState);
    			prevFrame = frame;
    		}
    	}

    	//******************************************************************************************************************
    	//  Store listener methods
    	//******************************************************************************************************************

    	@autobind
    	onError(store, msg, error) {
    		// mark current active frame as not active
    		this.activeFrame.active = false;

    		this.currFrame.onError(msg, error);
    		this.frames.push(this.currFrame);
    		this.truncateFrames();

    		this.activeFrame = this.currFrame;
    		this.currFrame = new LogFrame(store, this.nextFrameId++);

    		console.warn(`Store error: msg=${msg}, error=${error}`);
    		error && error.stack && console.warn(error.stack);
    	}

    	@autobind
    	onPostStateUpdate(store, action, impl) {
    		this.currFrame.addAction(action, impl);

    		if (this.traps) {
    			const traps = Object.values(this.traps);
    			const nextState = store.shadow.__().nextState();

    			for (let i=0, t; t=traps[i]; i++) {
    				var value = isString(t.eval)
    					?result(nextState, t.eval)
    					:t.eval(nextState)

    				if (isEqual(value, t.value)) {
    					debugger;

    					this.clearTrap(t.name);
    				}
    			}
    		}
    	}

    	@autobind
    	onPostUpdate(store, time, currState, prevState) {
    		const activeIdx = this.frames.indexOf(this.activeFrame);

    		// remove all frames after current active frame (time travel occured)
    		if (activeIdx < this.frames.length - 1) {
    			this.frames = this.frames.slice(0, activeIdx + 1);
    		}

    		// mark current active frame as not actives
    		this.activeFrame.active = false;

    		// complete the current frame and mark active then create a new current frame
    		this.currFrame.completed(time, currState);
    		this.frames.push(this.currFrame);
    		this.truncateFrames();

    		this.activeFrame = this.currFrame;
    		this.currFrame = new LogFrame(store, this.nextFrameId++);
    	}

    	//******************************************************************************************************************
    	//  private methods
    	//******************************************************************************************************************

    	truncateFrames() {
    		if (this.maxFrames > 0 && this.maxFrames < this.frames.length) {
    			this.frames = this.frames.slice(this.frames.length - this.maxFrames);
    		}
    	}

    	//******************************************************************************************************************
    	//  time travel
    	//******************************************************************************************************************

    	get index() {
    		return this.frames.indexOf(this.activeFrame);
    	}

    	get size() {
    		return this.frames.length;
    	}

    	back() {
    		if (this.index === 0) { return }

    		this.goto(this.index - 1);
    	}

    	forward() {
    		if (this.index + 1 === this.size) { return }

    		this.goto(this.index + 1);
    	}

    	goto(idx) {
    		if (idx === this.idx) {
    			return
    		} else if (this.size <= 1) {
    			return console.log(`Time travel error - no log frames`);
    		} else if (idx < 0 || idx >= this.size) {
    			return console.log(`Time travel error - index=${idx}, expected index between 0 - ${this.size-1}`);
    		}

    		const currActiveFrame = this.activeFrame;
    		const nextActiveFrame = this.frames[idx];
    		const store = this.store;

    		store.changeState(nextActiveFrame.state, true, nextActiveFrame.time);

    		currActiveFrame.active = false;
    		nextActiveFrame.active = true;

    		this.activeFrame = nextActiveFrame;
    	}
    }

    /** @ignore */
    export class LogFrame {
    	constructor(store, id) {
    		this.id = id;
    		this.store = store;
    		this.actions = [];
    		this.active = false;
    	}

    	activate() {
    		invariant(has(this, state), "LogFrame does not have a state");

    		this.store.changeState(this.state, true);
    		this.active = true;
    	}

    	addAction(action, impl) {
    		this.actions.push(new FrameAction(action, impl));
    	}

    	captureState() {
    		this.state = this.store.state;
    		this.active = true;
    		this.time = this.store.updateTime;
    		this.captureTime = new Date();
    	}

    	completed(time, currState) {
    		this.time = time;
    		this.state = currState;
    		this.active = true;
    		this.captureTime = new Date();
    	}

    	inactivate() {
    		this.active = false;
    	}

    	onError(msg, error) {
    		this.state = this.store.state;
    		this.active = true;
    		this.msg = msg;
    		this.error = error;
    		this.captureTime = new Date();
    	}

    	print(filter=null, prevFrame, printState=true) {
    		var when = !this.captureTime
    			?"n/a"
    			:!prevFrame ?this.captureTime.toLocaleString() :`+${this.captureTime - prevFrame.captureTime} ms`

    		if (printState) {
    			console.log(`\tID=${this.id}, active=${this.active}, completed=${when}, final state:`, this.state);
    		} else {
    			console.log(`\tID=${this.id}, active=${this.active}, completed=${when}`);
    		}

    		var actions = sortBy(this.actions, a => {
    				const path = a.impl.dotPath();

    				return path=="root" ?0 :path;
    			});

    		if (filter) {
    			actions = actions.filter(filter);
    		}

    		if (actions.length == 0) {
    			console.log(`\t\tNo actions ${ filter ?"using filter" :""}`)
    		}

    		for (let i=0, action; action=actions[i]; i++) {
    			action.print(printState);
    		}

    		if (this.error) {
    			const stack = this.error.stack ?this.error.stack :"n/a";

    			console.log(`\t\tEnded in error: msg=${this.msg}, error=%O, stack=%O`, this.error, stack);
    		}
    	}
    }

    /** @ignore */
    export class FrameAction {
    	constructor(action, impl) {
    		this.action = action;
    		this.impl = impl;
    	}

    	print(printState=true) {
    		const { action, impl } = this;

    		if (printState) {
    			console.log(`\t\t${impl.dotPath()}::${action.name}, replace=${!!action.replace} nextState=%O, startState=%O`,
    					action.nextState, impl.state());
    		} else {
    			console.log(`\t\t${impl.dotPath()}::${action.name}, replace=${!!action.replace}`);
    		}
    	}
    }

    export function createConsoleLogger(logger) {
    	return {
    		get back() {
    			return logger.back();
    		},

    		get clear() {
    			logger.clear();
    		},

    		get forward() {
    			return logger.forward();
    		},

    		get help() {
    			console.log(HelpMsg+logger.name+"\n\n");
    		},

    		get index() {
    			return logger.index;
    		},

    		get maxFrames() {
    			return logger.maxFrames;
    		},

    		get next() {
    			return this.forward();
    		},

    		get print() {
    			logger.print();
    		},

    		get printNoState() {
    			logger.print(false);
    		},

    		get size() {
    			return logger.size;
    		},

    		get store() {
    			return logger.store;
    		},

    		clearTrap(name) {
    			logger.clearTrap(name);
    		},

    		setActionFilter(filter) {
    			logger.setActionFilter(filter);
    		},

    		setMaxFrames(maxFrames) {
    			logger.setMaxFrames(maxFrames);
    		},

    		setTrap(condition, value, name) {
    			return logger.setTrap(condition, value, name);
    		},

    		tail(count, printState) {
    			logger.tail(count, printState);
    		},

    		goto(idx) {
    			logger.goto(idx);
    		},
    	}
    }