import {IS_CLIENT} from "@gongt/ts-stl-library/check-environment";
import {createLogger} from "@gongt/ts-stl-library/debug/create-logger";
import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels";
import {AnyAction, Reducer} from "redux";
import {IAction, IAData, ISingleReducer} from "./action";

export type AnyStore = any;
export type AnyActionData = any;

export interface IReducerInfo<T=AnyStore> {
	callback: ISingleReducer<T, AnyActionData>;
	actionName: string; // action name
	storeName: string; // store name
	global: boolean;
}

export interface IReducerMap {
	[actionName: string]: {
		[storeName: string]: {
			local: ISingleReducer<AnyStore, AnyActionData>[];
			global: ISingleReducer<AnyStore, AnyActionData>[];
		};
	};
}

export interface ISingleReducerChanging<ValueInterface, IData extends IAData> {
	(part: string, state: ValueInterface, rawAction: IAction<IData>): boolean;
	
	displayName?: string;
}

export interface IReducerMapCombined {
	[actionName: string]: {
		[storeName: string]: ISingleReducerChanging<AnyStore, AnyActionData>;
	};
}

function callAll<T>(reducers: ISingleReducerChanging<T, AnyActionData>[]): ISingleReducerChanging<T, AnyAction> {
	return function callAllWrapper(part: string, state: any, action) {
		let changed = false;
		for (const reducer of reducers) {
			debug('[all] call reducer (%s): %s', reducers.length, reducer.displayName || reducer.name);
			const r = reducer(part, state, action);
			if (r) {
				debug('  state changed.');
				changed = true;
			}
		}
		return changed;
	};
}

function callOnlyOne<T>(reducers: ISingleReducer<T, AnyActionData>[],
                        storeName: string): ISingleReducerChanging<T, AnyAction> {
	return function callOneWrapper(part: string, state: any, action) {
		for (const reducer of reducers) {
			debug('[one] call reducer: %s -> %s', storeName, reducer.displayName || reducer.name);
			const ret = reducer(state[storeName], action.payload, action);
			if (ret) {
				debug('  action handled: %O', ret);
				if (state[storeName] !== ret) {
					state[storeName] = ret;
				}
				return true;
			}
		}
		return false;
	};
}

function combineAll(reducers: IReducerMap): IReducerMapCombined {
	const ret: IReducerMapCombined = {};
	debugStart('Init Actions');
	const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null;
	const statics = [];
	for (const act of Object.keys(reducers)) {
		ret[act] = {};
		const allCallbackCurrentAct = [];
		for (const sto of Object.keys(reducers[act])) {
			const globals = reducers[act][sto].global;
			const locals = reducers[act][sto].local;
			if (locals.length) {
				ret[act][sto] = callOnlyOne(locals, sto);
				if (debugStart.enabled) {
					statics.push({action: act, store: sto, type: 'local', listeners: locals.length,});
				}
			}
			if (globals.length) {
				allCallbackCurrentAct.push(callOnlyOne(globals, sto));
				if (debugStart.enabled) {
					statics.push({action: act, store: sto, type: 'global', listeners: globals.length,});
				}
			}
		}
		if (allCallbackCurrentAct.length) {
			ret[act]['*'] = callAll(allCallbackCurrentAct);
			
			if (debugStart.enabled) {
				statics.push({action: act, store: '*', type: 'global', listeners: allCallbackCurrentAct.length,});
			}
		}
	}
	
	if (debugStart.enabled) {
		console.table(statics);
	}
	debugEnd();
	return ret;
}

function createStructure(reducers: IReducerInfo[]): IReducerMap {
	const data: IReducerMap = {};
	for (const info of reducers) {
		const {actionName: act, storeName: sto} = info;
		if (!data[act]) {
			data[act] = {};
		}
		if (!data[act][sto]) {
			data[act][sto] = {local: [], global: []};
		}
		const mod = info.global? 'global' : 'local';
		data[act][sto][mod].push(info.callback);
	}
	return data;
}

export function reducerFromNative<T>(native: Reducer<T>): ISingleReducer<T, any> {
	if (native['__wrappedNativeReducer']) {
		return native;
	}
	const nativeReducerWrapper = function (state, action, rawAction) {
		debug('call native reducer: %s[%O]', native['displayName'] || native.name, native);
		const ret = native(state, rawAction);
		if (ret === state) {
			return undefined;
		}
		return ret;
	};
	nativeReducerWrapper['displayName'] = `nativeReduce(${native['displayName'] || native.name})`;
	nativeReducerWrapper['__wrappedNativeReducer'] = true;
	return nativeReducerWrapper;
}

const debugStart = createLogger(LOG_LEVEL.DEBUG, 'reducer');
if (IS_CLIENT) {
	debugStart.fn = console.groupCollapsed.bind(console);
}
const debug = createLogger(LOG_LEVEL.INFO, 'reducer');

export function MyCombineReducers<AppState, T=keyof AppState>(reducers: IReducerInfo<T>[],
                                                              toplevel?: Reducer<AppState>): Reducer<AppState> {
	
	for (const item of reducers) {
		if (!item.actionName || !item.storeName || !item.callback) {
			console.error(item);
			throw new TypeError('final check: invalid reducer. more info see console.');
		}
	}
	const reducersData = createStructure(reducers);
	const reducersCombine = combineAll(reducersData);
	// const storeList = reducers.map(info => info.storeName);
	
	return (state: AppState, action: IAction<any>) => {
		if (!action || !action.type) {
			throw new TypeError('dispatching action without type.');
		}
		debugStart('action: %s @ %s', action.type, action.virtualStorage || 'Nil');
		const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null;
		debug(action);
		
		try {
			if (toplevel) { // topLevel: original redux top level style reducer
				const newState = toplevel(state, action);
				if (state !== newState) {
					debug('handled by top level reducer - finish');
					debugEnd();
					return newState;
				}
			}
			
			let {type: actionType, virtualStorage: store, payload} = action;
			if (!reducersCombine[actionType]) {
				debug('unknown action - finish');
				debugEnd();
				return state;
			}
			
			if (!action.hasOwnProperty('payload')) {
				debug('action has no payload');
				debugEnd();
				return state; // this is normal action
			}
			
			let changed = false;
			const currentReducers = reducersCombine[actionType];
			const storeName = store;
			debug('sub store name: %s', storeName);
			
			/**
			 * global reducers:
			 * only call reducer witch:
			 *    1. action is watching by reducer
			 * if any reducer modified the state:
			 *   NO any more reducer will called within this virtual store
			 * every action may or may-not call multiple global reducer.
			 *   every action will call at most only one global reducer on each virtual store
			 */
			const global = currentReducers['*'];
			if (global) { // global: call any store's reducer, but
				debug(' -> call global reducers');
				const thisChanged = global(storeName, state, action);
				changed = thisChanged || changed;
			} else {
				debug(' -> no global reducer');
			}
			
			/**
			 * local reducers:
			 * only call reducer witch:
			 *    1. action is watching by reducer
			 *    2. action.store equals to reducer.store
			 * if any reducer modified the state:
			 *   NO any more reducer will called
			 * every action will only call one local reducer.
			 */
			const local = currentReducers[store];
			if (local) {
				debug(' -> call local reducers');
				const thisChanged = local(storeName, state, action);
				changed = thisChanged || changed;
			} else {
				debug(' -> no local reducer');
			}
			
			debug('state %s', changed? 'changed' : 'NOT changed');
			debugEnd();
			return changed? Object.assign({}, state) : state;
		} catch (e) {
			debugEnd();
			debugEnd();
			debugEnd();
			debug('error while processing action: %s', e? e.message : 'no info');
			throw e;
		}
	};
}
