import * as PropTypes from "prop-types";
import {connect, MapStateToProps, Options} from "react-redux";
import {Dispatch} from "redux";
import {BaseComponent, BaseComponentConstructor, StatefulBaseComponentConstructor} from "../react/stateless-component";
import {IReduxActionConstructor} from "./action";
import {IState} from "./preload-state";
import {IVirtualStoreConstructor} from "./virtual-store";

export interface TDispatchProps {
	_dispatch?: Dispatch<any>;
	renderCounts?: number;
}

export function tDispatchMapper(disp: Dispatch<any>): TDispatchProps {
	const ret = {};
	Object.defineProperty(ret, '_dispatch', {
		value: disp,
		configurable: false,
		enumerable: true,
		writable: false,
	});
	return ret;
}

export type WrapFunction<In, Out> = (state: In) => Out;
export type MapperObject<State extends IState, Props> = {
	[K in keyof Props]?: WrapFunction<State, Props[K]>;
	};
export type MapperFunction<State extends IState, Props> = WrapFunction<State, Partial<Props>>;
export type Mapper<State extends IState, Props> = MapperFunction<State, Props>|MapperObject<State, Props>;

// MapperObject<Props, State>;

export class ReactReduxConnector<State extends IState, Props extends TDispatchProps> {
	protected mps: Mapper<State, Props>[] = [];
	
	protected advOpts: Options<any, any, any> = {
		renderCountProp: 'renderCounts',
		shouldHandleStateChanges: true,
		withRef: false,
		pure: true,
	};
	
	constructor() {
		this.connect = this.connect.bind(this);
	}
	
	addMapper(obj: Mapper<State, Props>) {
		this.mps.push(obj);
	}
	
	isComponentUseContext(notPure: boolean = true) {
		this.advOpts.pure = !notPure;
	}
	
	isComponentUseDOM(storeRef: boolean = true) {
		this.advOpts.withRef = storeRef;
	}
	
	connect<T extends object, Class extends StatefulBaseComponentConstructor<Props, T>>(reactClass: Class): Class {
		if (reactClass['_redux_connect']) {
			throw new TypeError(`duplicate @ReduxConnector() on ${reactClass.displayName || reactClass.name}`);
		}
		
		prepareReactClass(reactClass);
		
		let mapper: MapStateToProps<Props, void, State>;
		if (this.mps.length === 0) {
			this.advOpts.shouldHandleStateChanges = false;
			mapper = undefined;
		} else {
			this.advOpts.shouldHandleStateChanges = true;
			mapper = <any> createMapper(this.mps);
		}
		
		this.advOpts.getDisplayName = (name) => {
			return `RRConnector(${name}) [pure=${this.advOpts.pure},withRef=${this.advOpts.withRef},handle=${this.advOpts.shouldHandleStateChanges}]`;
		};
		
		const c: ClassDecorator = <any>connect<Props, TDispatchProps, void>(
			mapper,
			tDispatchMapper, // TODO
			undefined, // TODO
			this.advOpts,
		);
		
		const RetClass = <any>c(reactClass);
		Object.defineProperty(RetClass, '_redux_connect', {
			enumerable: false,
			configurable: false,
			writable: false,
			value: true,
		});
		// RetClass.displayName = `ReactReduxConnector(${reactClass.displayName || reactClass.name})`;
		
		return RetClass;
	}
}

function prepareReactClass<Props extends TDispatchProps>(reactClass: BaseComponentConstructor<Props>) {
	if (!reactClass.propTypes) {
		reactClass.propTypes = {};
	}
	
	reactClass.propTypes.renderCounts = PropTypes.number;
}

/** @deprecated */
export function connectToStore<State extends IState, Props extends TDispatchProps>
(mapper0: WrapFunction<State, Props>);
/** @deprecated */
export function connectToStore<State extends IState, Props extends TDispatchProps>
(...mappers: Mapper<State, Props>[]);
/** @deprecated */
export function connectToStore<State extends IState, Props extends TDispatchProps>
(...mappers: Mapper<State, Props>[]) {
	const conn = new ReactReduxConnector<State, Props>();
	for (const mapper of mappers) {
		conn.addMapper(mapper);
	}
	return conn.connect.bind(conn);
}

function createMapper<State extends IState, Props>(mappers: Mapper<State, Props>[]): MapperFunction<State, Props> {
	if (mappers.length === 1 && typeof mappers[0] === 'function') {
		return <any>mappers[0];
	}
	// let mapper: MapStateToPropsParam<Props, void>;
	const fns: MapperFunction<State, Props>[] = <any>mappers.filter((mapObject) => {
		return typeof mapObject === 'function';
	});
	const objects = Object.assign({}, ...mappers.filter((mapObject) => {
		return typeof mapObject !== 'function';
	}));
	const keys: (keyof Props)[] = <any>Object.keys(objects);
	if (keys.length) {
		fns.push((state: State) => {
			const props: Partial<Props> = {};
			for (const i of keys) {
				props[i] = objects[i](state);
			}
			return props;
		});
	}
	return (data: State): Props => {
		const ret: Props = <any>{};
		for (const fn of fns) {
			Object.assign(ret, fn(data));
		}
		return ret;
	};
}

export type triggerFn<IData> = (this: BaseComponent<TDispatchProps>, args: IData) => void;

export function ActionDispatcher<IData>
(Act: IReduxActionConstructor<IData>): PropertyDecorator;
export function ActionDispatcher<IData, VI>
(Act: IReduxActionConstructor<IData, VI>, Sto: IVirtualStoreConstructor<VI>): PropertyDecorator;
export function ActionDispatcher<IData, VI=void>
(Act: IReduxActionConstructor<IData, VI>, Sto?: IVirtualStoreConstructor<VI>): PropertyDecorator {
	return function (this: BaseComponent<any>, name: string) {
		if (this[name]) {
			throw new Error('can not use @ActionDispatch with property with value');
		}
		this[name] = <triggerFn<IData>>function (this, value) {
			this.props._dispatch(new Act(value, Sto).toJSON());
		};
	};
}

export type triggerMtd<IData> = (this: BaseComponent<TDispatchProps>, ...args: any[]) => IData;
export type TypedMethodDecorator<T> = (target: Object,
                                       propertyKey: string|symbol,
                                       descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
export type TriggerDecorator<IData> = TypedMethodDecorator<triggerMtd<IData>>;

export const CANCEL_TRIGGER = null;

export function ActionTrigger<IData>
(Act: IReduxActionConstructor<IData>): TriggerDecorator<IData>;
export function ActionTrigger<IData, VI>
(Act: IReduxActionConstructor<IData, VI>,
 Sto: IVirtualStoreConstructor<VI>): TriggerDecorator<IData>;
export function ActionTrigger<IData, VI>
(Act: IReduxActionConstructor<IData, VI>,
 Sto?: IVirtualStoreConstructor<VI>): TriggerDecorator<IData> {
	return (target: BaseComponentConstructor<any>, name, descriptor) => {
		if (!descriptor || !descriptor.value || typeof descriptor.value !== 'function') {
			throw new TypeError('ActionTrigger: only allow decorate method.');
		}
		const original = descriptor.value;
		
		function trigger(this: BaseComponent<any>, ...args: any[]) {
			const ret = original.apply(this, args);
			if (ret !== undefined && ret !== CANCEL_TRIGGER) {
				this.props._dispatch(new Act(ret, Sto).toJSON());
			}
			return ret;
		}
		
		return {
			value: <triggerMtd<IData>>trigger,
		};
	};
}
