// Import the core angular services.
import { AbstractControl } from "@angular/forms";
import { AbstractControlDirective } from "@angular/forms";
import { Directive } from "@angular/core";
import { EventEmitter } from "@angular/core";
import { NgForm } from "@angular/forms";
import { NgModel } from "@angular/forms";
import { NgModelGroup } from "@angular/forms";
import { Optional } from "@angular/core";
import { Self } from "@angular/core";
import { Subscription } from "rxjs";
 
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
 
export interface ReactiveBridgeEvent {
	type: "statusChange" | "valueChange";
	target: AbstractControl;
	currentValue: any;
	previousValue: any | undefined;
}
 
@Directive({
	selector: "form[reactiveBridge],[ngModelGroup][reactiveBridge],[ngModel][reactiveBridge]",
	outputs: [
		"statusChangeEvents: statusChange",
		"valueChangeEvents: valueChange"
	],
	exportAs: "reactiveBridge"
})
export class ReactiveBridgeDirective {
 
	public statusChangeEvents: EventEmitter<ReactiveBridgeEvent>;
	public valueChangeEvents: EventEmitter<ReactiveBridgeEvent>;
 
	private control: AbstractControl | null;
	private controlDirective: AbstractControlDirective;
	private isDestroyed: boolean;
	private previousStatus: any | undefined;
	private previousValue: any | undefined;
	private subscriptions: Subscription[];
 
	// I initialize the reactive-bridge directive.
	// --
	// NOTE: Since this directive can be applied to three different types of elements,
	// we're going to injected all three in the SELF scope and just use whichever one
	// is defined.
	constructor(
		@Self() @Optional() ngForm: NgForm,
		@Self() @Optional() ngModelGroup: NgModelGroup,
		@Self() @Optional() ngModel: NgModel
		) {
 
		this.controlDirective = ( ngForm || ngModelGroup || ngModel ) !;
 
		this.control = null;
		this.isDestroyed = false;
		this.previousStatus = undefined;
		this.previousValue = undefined;
		this.statusChangeEvents = new EventEmitter();
		this.subscriptions = [];
		this.valueChangeEvents = new EventEmitter();
 
	}
 
	// ---
	// PUBLIC METHODS.
	// ---
 
	// I get called when the directive is being destroyed.
	public ngOnDestroy() : void {
 
		this.isDestroyed = true;
 
		for ( var subscription of this.subscriptions ) {
 
			subscription.unsubscribe();
 
		}
 
	}
 
 
	// I get called once after the inputs have been bound for the first time.
	public ngOnInit() : void {
 
		// Since the NgForm and NgModel directives create internal controls as part of
		// their initialization, the underlying control will be available immediately.
		if ( this.controlDirective.control ) {
 
			this.control = this.controlDirective.control;
			this.setupSubscriptions();
			return;
 
		}
 
		// If we made it this far, we're dealing with the NgModelGroup. Unlike the other
		// form directives, this one has to register itself with the form asynchronously
		// for reasons that I cannot fully understand when reading the Angular source
		// code. That said, it seems that deferring the initialization with a Promise
		// aligns with the workflow that the NgModelGroup directive is using internally.
		// --
		// NOTE: If we tried to initialize all three types of directives inside the same
		// Promise-based workflow, the NgModelGroup wouldn't fire on form-load. I have
		// no idea why. I assume it is a weird race-condition somewhere.
		Promise.resolve().then(
			() => {
 
				// If the Promise resolves after the directive is destroyed, skip the
				// subscriptions configuration.
				if ( this.isDestroyed ) {
 
					return;
 
				}
 
				this.control = this.controlDirective.control;
				this.setupSubscriptions();
 
			}
		);
 
	}
 
	// ---
	// PRIVATE METHODS.
	// ---
 
	// I setup the subscriptions on the underlying control's Reactive streams so that we
	// can power the EventEmitters on the bridge.
	private setupSubscriptions() : void {
 
		this.subscriptions.push(
			this.control.statusChanges.subscribe(
				( event ) => {
 
					this.statusChangeEvents.emit({
						type: "statusChange",
						target: this.control,
						previousValue: this.previousStatus,
						currentValue: event
					});
 
					this.previousStatus = event;
 
				}
			),
			this.control.valueChanges.subscribe(
				( event ) => {
 
					this.valueChangeEvents.emit({
						type: "valueChange",
						target: this.control,
						previousValue: this.previousValue,
						currentValue: event
					});
 
					this.previousValue = event;
 
				}
			)
		);
 
	}
 
}