import { ApplicationRef, Injectable, NgZone } from '@angular/core';
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
import { DatasetProfileFieldViewStyle } from '@app/core/common/enum/dataset-profile-field-view-style';
import { isNumeric } from '@app/utilities/enhancers/utils';
import { Observable, Subject } from 'rxjs';
import { Rule } from '../../../../core/model/dataset-profile-definition/rule';
import { VisibilityRule } from './models/visibility-rule';
import { VisibilityRuleSource } from './models/visibility-rule-source';
import { VisibilityRulesContext } from './models/visibility-rules-context';

@Injectable()
export class VisibilityRulesService {

	private readonly VISIBILITY_RULE_LOGIC: 'OR' | 'AND' = 'OR';
	private readonly DEFAULTVISIBILITY = false;

	private visibilityRuleContext: VisibilityRulesContext;
	private form: AbstractControl;
	public elementVisibilityMap = new Map<String, boolean>();
	private elementVisibilityMapSubject = new Subject<Map<String, boolean>>();
	private elementComputationalMap = new Map<String, Map<String, boolean>>(); /// keep saved the values of each form control validity value
	private _changeMade$ = new Subject<void>();


	constructor(
		public applicationReference: ApplicationRef,
		public ngZone: NgZone
	) {

	}

	getElementVisibilityMapObservable(): Observable<Map<String, boolean>> {
		return this.elementVisibilityMapSubject.asObservable();
	}

	public checkElementVisibility(id: string): boolean {
		if (this.visibilityRuleContext.rules.filter(item => item.targetControlId === id).length === 0) { return true; }
		return this.elementVisibilityMap.has(id) ? this.elementVisibilityMap.get(id) : false;
	}

	public buildVisibilityRules(item: Array<Rule>, form: AbstractControl) {
		this.visibilityRuleContext = new VisibilityRulesContext();
		this.visibilityRuleContext.buildVisibilityRuleContext(item || []);
		this.form = form;
		this.resetVisibilityRules();
	}

	public updateValueAndVisibility(id: string, value: any) {
		const visibilityRules = this.visibilityRuleContext.rules.filter(item => item.sourceVisibilityRules.filter(source => source.sourceControlId === id).length > 0);
		if (visibilityRules.length > 0) {
			visibilityRules.forEach(item => this.evaluateVisibility(item, value, id));
			this.elementVisibilityMapSubject.next(this.elementVisibilityMap);
		}
	}

	private evaluateVisibility(visibilityRule: VisibilityRule, value: any, sourceId: string) {// source controlId is the same

		const targetId = visibilityRule.targetControlId;
		const visibilityMap = this.elementComputationalMap.get(targetId) ? this.elementComputationalMap.get(targetId) : new Map<String, boolean>();


		if (value instanceof Array) {

			const parsedSourceControlValues = visibilityRule.sourceVisibilityRules.map(e => this.parseValue(e.sourceControlValue));
			const parsedValues = value.map(e => this.parseValue(e));

			const isVisible = parsedValues.map(v => parsedSourceControlValues.includes(v)).reduce((acc, current) => acc || current, false);


			// if(isVisible){
			// 	this._emitChangesIfNeeded(visibilityRule.targetControlId, true);
			// 	this.elementVisibilityMap.set(visibilityRule.targetControlId, true);
			// 	return;
			// }
			visibilityMap.set(sourceId, isVisible);

		} else {
			const visibilityDependencySource = visibilityRule.sourceVisibilityRules.filter(x => x.sourceControlId === sourceId);

			const shouldBeVisible = visibilityDependencySource.reduce((isVisible, x) => {

				const shouldBeHidden = value !== null && (this.parseValue(value) !== this.parseValue(x.sourceControlValue));
				return this.VISIBILITY_RULE_LOGIC === 'OR' ? (isVisible || !shouldBeHidden) : (isVisible && !shouldBeHidden);
				// if(value !== null && )
			}, this.VISIBILITY_RULE_LOGIC === 'AND');
			visibilityMap.set(sourceId, shouldBeVisible);
		}


		this.elementComputationalMap.set(targetId, visibilityMap);// unnessecary


		const isVisible = this._computeVisibility(targetId);
		this._emitChangesIfNeeded(targetId, isVisible);
		const previousVisibility = this.elementVisibilityMap.get(targetId);
		this.elementVisibilityMap.set(targetId, isVisible);

		if (!isVisible && previousVisibility !== isVisible) {
			this.resetControlWithId(this.form, targetId);
		}


		// for (let i = 0; i < visibilityRule.sourceVisibilityRules.length; i++) {
		// 	if (value != null && (this.parseValue(value) !== this.parseValue(visibilityRule.sourceVisibilityRules[i].sourceControlValue))) {
		// 		this._emitChangesIfNeeded(visibilityRule.targetControlId, false);
		// 		this.elementVisibilityMap.set(visibilityRule.targetControlId, false);
		// 		this.resetControlWithId(this.form, visibilityRule.targetControlId);
		// 		//this.updateValueAndVisibility(visibilityRule.targetControlId, null);
		// 		// this.clearValues(targetPathKey);
		// 		return;
		// 	}
		// }
		// this._emitChangesIfNeeded(visibilityRule.targetControlId, true);
		// this.elementVisibilityMap.set(visibilityRule.targetControlId, true);

		// this.updateValueAndVisibility(visibilityRule.targetControlId, null);
	}


	private _computeVisibility(targetId: string): boolean {
		const visibilityMap = this.elementComputationalMap.get(targetId);
		const values = visibilityMap.values();
		let currentVal = values.next();
		let visibilityValues: boolean[] = [];
		while (!currentVal.done) {
			visibilityValues.push(currentVal.value);
			currentVal = values.next();
		}


		if (visibilityValues.length) {
			return visibilityValues.reduce((r, c) => {
				if (this.VISIBILITY_RULE_LOGIC === 'OR') {
					return r || c;
				} else {
					return r && c;
				}
			}, visibilityValues[0]);
		}

		return this.DEFAULTVISIBILITY;
	}

	private resetVisibilityRules() {
		this.elementVisibilityMap.clear();
		this.elementVisibilityMap = new Map<String, boolean>();
		this.elementComputationalMap.clear();
		this.elementComputationalMap = new Map<String, Map<String, boolean>>();
		this._populateComputationMap(); /// !IMPORTANT FOR THE AND LOGIC
		this._changeMade$.next();
	}

	private _populateComputationMap(): void {
		this.visibilityRuleContext.rules.forEach(rule => {
			const targetId = rule.targetControlId;
			const visibilityMap = this.elementComputationalMap.get(targetId) ? this.elementComputationalMap.get(targetId) : new Map<String, boolean>();
			rule.sourceVisibilityRules.forEach(vr => {
				visibilityMap.set(vr.sourceControlId, this.DEFAULTVISIBILITY);
			});
			this.elementComputationalMap.set(targetId, visibilityMap);
		});
	}

	parseValue(value: any) {
		if (typeof value === 'string') {
			if (isNumeric(value)) { return value; }
			else if (value === 'true') {
				return true;
			}
			else if (value === 'false') {
				return false;
			}
			else { return this.translate(value); }
		} else { return value; }
	}

	search(path, obj, target) {
		for (const k in obj) {
			if (obj.hasOwnProperty(k)) {
				if (obj[k] === target) {
					return path + '.' + k;
				} else if (typeof obj[k] === 'object') {
					const result = this.search(path + '.' + k, obj[k], target);
					if (result) {
						return result;
					}
				}
			}
		}
		return false;
	}

	scanIfChildsOfCompositeFieldHasVisibleItems(compositeFieldParent: FormGroup): boolean {
		let isVisible = false;
		(<FormArray>(compositeFieldParent.get('fields'))).controls.forEach(element => {
			if (this.checkElementVisibility(element.get('id').value)) {
				isVisible = true;
			}
		});
		return isVisible;
	}

	private translate(item: any) {
		try {
			return JSON.parse(item).value;
		} catch (error) {
			return item;
		}
	}

	private resetControlWithId(formControl: AbstractControl, id: string) {
		if (formControl instanceof FormGroup) {
			if ((formControl as FormGroup).contains('id') && (formControl as FormGroup).contains('value') && (formControl as FormGroup).get('id').value === id) {
				this.resetFieldFormGroup(formControl);
			} if ((formControl as FormGroup).contains('id') && (formControl as FormGroup).contains('fields') && (formControl as FormGroup).get('id').value === id) {
				this.resetCompositeFieldFormGroup(formControl);
			} else {
				Object.keys(formControl.controls).forEach(item => {
					const control = formControl.get(item);
					this.resetControlWithId(control, id);
				});
			}
		} else if (formControl instanceof FormArray) {
			formControl.controls.forEach(item => {
				this.resetControlWithId(item, id);
			});
		}
	}

	private resetFieldFormGroup(formGroup: FormGroup) {
		const renderStyle = formGroup.getRawValue().viewStyle.renderStyle;
		if (renderStyle === DatasetProfileFieldViewStyle.Validation || renderStyle === DatasetProfileFieldViewStyle.DatasetIdentifier) {
			formGroup.get('value').setValue({ identifier: '', type: '' });
		} else {
			formGroup.get('value').setValue(formGroup.get('defaultValue').value ? this.parseValue(formGroup.get('defaultValue').value.value) : undefined);
		}

	}

	private resetCompositeFieldFormGroup(formGroup: FormGroup) {
		(formGroup.get('fields') as FormArray).controls.forEach((element: FormGroup) => {
			this.resetFieldFormGroup(element);
		});
		(formGroup.get('multiplicityItems') as FormArray).controls.splice(0);
	}
	private _emitChangesIfNeeded(id: string, valueToBeSet: boolean) {
		if (this.elementVisibilityMap.has(id)) {
			if (this.elementVisibilityMap.get(id) != valueToBeSet) {
				this._changeMade$.next();
			}
		} else {
			this._changeMade$.next();
		}
	}
	public get visibilityChange() {
		return this._changeMade$.asObservable();
	}
	public getVisibilityDependency(targetId: string): VisibilityRuleSource[] | null {
		return this.visibilityRuleContext.rules.reduce((hasDependency, rule) => {
			if (hasDependency) return hasDependency;

			if (rule.targetControlId === targetId) {
				return rule.sourceVisibilityRules;
			}

			return null;
		}, null) as VisibilityRuleSource[];
	}

	public getVisibilityTargets(sourceId: string): string[] {
		return this.visibilityRuleContext.rules.filter(x => {
			const result = x.sourceVisibilityRules.filter(y => y.sourceControlId === sourceId);
			return result.length;
		}).map(x => x.targetControlId);
	}

	// public appendVisibilityRule(rule: VisibilityRule): void{

	// 	const existingTargetRule = this.visibilityRuleContext.rules.find( r => r.targetControlId === rule.targetControlId);

	// 	if(existingTargetRule){
	// 		rule.sourceVisibilityRules.forEach(svr =>{
	// 			existingTargetRule.sourceVisibilityRules.push(svr);
	// 		});
	// 	}else{
	// 		this.visibilityRuleContext.rules.push(rule);
	// 	}


	// }


	//removes rule that has the specific id either as a source either as a target
	public removeAllIdReferences(id: string): void {

		// * Remove from visibility rues and visibility rules context

		//remove as a target
		const temp = this.visibilityRuleContext.rules.map((x, i) => (x.targetControlId === id) ? i : null);
		const indexes = temp.filter(x => x !== null);
		indexes.reverse().forEach(index => this.visibilityRuleContext.rules.splice(index, 1));
		this.elementVisibilityMap.delete(id);



		//remove as a source
		const tbd = this.visibilityRuleContext.rules.reduce((to_be_deleted, rule, ruleIdx) => {
			const idxs = rule.sourceVisibilityRules.map((x, i) => (x.sourceControlId === id) ? i : null).filter(x => x !== null);
			idxs.reverse().forEach(index => rule.sourceVisibilityRules.splice(index, 1));

			if (!rule.sourceVisibilityRules.length) {
				to_be_deleted.push(ruleIdx);
			}
			return to_be_deleted
		}, []);


		//clean up empty
		tbd.reverse().forEach(index => {
			this.visibilityRuleContext.rules.splice(index, 1);
		});



		// * Remove from computational map

		// as a target
		if (this.elementComputationalMap.get(id)) {
			this.elementComputationalMap.delete(id);
		}


		// as a source
		const keyIterator = this.elementComputationalMap.keys();
		let currentKey = keyIterator.next();


		while (!currentKey.done) {
			const currentVals = this.elementComputationalMap.get(currentKey.value);
			currentVals.delete(id);
			currentKey = keyIterator.next();
		}
	}


	public addNewRule(rule: Rule, currentVisibility = this.DEFAULTVISIBILITY): void {
		const targetId = rule.targetField;
		const sourceId = rule.sourceField;
		console.log(rule);
		this.visibilityRuleContext.addToVisibilityRulesContext(rule);


		let visibilityMap = this.elementComputationalMap.get(targetId);

		if (!visibilityMap) {
			visibilityMap = new Map<String, boolean>();
			this.elementComputationalMap.set(targetId, visibilityMap);
		}

		visibilityMap.set(sourceId, currentVisibility);
		const isVisible = this._computeVisibility(targetId);

		this._emitChangesIfNeeded(targetId, isVisible);
		this.elementVisibilityMap.set(targetId, isVisible);
		this.elementVisibilityMapSubject.next(this.elementVisibilityMap);
	}


	/**
	 * Check what sourceId hides or shows the target field
	 * return true if no rule found
	 */
	public checkTargetVisibilityProvidedBySource(sourceId: string, targetId: string): boolean {

		const computationalMap = this.elementComputationalMap.get(targetId);
		if (computationalMap) {
			return !!computationalMap.get(sourceId);
		}

		return true;
	}
}
