import { BooleanInput, coerceBooleanProperty, NumberInput, coerceNumberProperty } from '@angular/cdk/coercion';
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { AbstractControl, AsyncValidatorFn, UntypedFormControl, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { isEqual } from 'lodash';
import { Subject } from 'rxjs';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';

@Directive()
export abstract class FormBlock<TD, TC extends AbstractControl> implements OnInit, OnDestroy {
  private _readonly = false;

  @Input()
  get readonly(): boolean {
    return this._readonly;
  }
  set readonly(value: BooleanInput) {
    this._readonly = coerceBooleanProperty(value);
  }

  private _focusAfterInit = false;
  @Input()
  get focusAfterInit(): boolean {
    return this._focusAfterInit;
  }
  set focusAfterInit(value: BooleanInput) {
    this._focusAfterInit = coerceBooleanProperty(value);
  }

  readonly onDestroy$ = new Subject<void>();

  private _tabIndexStart: number;

  @Input()
  get tabIndexStart(): number {
    return this._tabIndexStart;
  }
  set tabIndexStart(value: NumberInput) {
    this._tabIndexStart = coerceNumberProperty(value);
  }

  abstract tabIndexEnd: NumberInput;

  private _data: TD;

  @Input()
  get data(): TD | undefined {
    return this._data;
  }
  set data(value: TD) {
    const previous = this._data;
    this._data = value;

    if (this.control && !isEqual(previous, value)) {
      this._patchValue({
        previous,
        current: value,
      });
    }
  }

  @Output()
  dataChanges = new EventEmitter<TD>();

  @Output()
  onInit = new EventEmitter<FormBlock<TD, TC>>();

  @Output()
  onDestroy = new EventEmitter<void>();

  control: TC;

  get value(): TD {
    return this.control.value;
  }

  get valid(): boolean {
    return this.control.valid;
  }

  ngOnInit(): void {
    this.initializeControl(this.data);
    this.control.valueChanges
      .pipe(takeUntil(this.onDestroy$), distinctUntilChanged(isEqual))
      .subscribe((value) => this.dataChanges.emit(value));
    this.onInit.emit(this);
  }

  ngOnDestroy(): void {
    this.onDestroy$.next();
    this.onDestroy$.complete();
    this.onDestroy.emit();
  }

  abstract initializeControl(data?: TD): void;

  abstract _patchValue(update: { previous: TD; current: TD }): void;
}

@Directive()
export abstract class FormBlockControl<TD> extends FormBlock<TD, UntypedFormControl> {
  @Input()
  requiredMark: boolean;

  private _validatorFns: ValidatorFn[];

  @Input()
  get validatorFns() {
    return this._validatorFns;
  }
  set validatorFns(value: ValidatorFn[]) {
    this._validatorFns = value;
    if (this.control) {
      this.updateValidators();
    }
  }

  private _asyncValidatorFns: AsyncValidatorFn[];

  @Input()
  get asyncValidatorFns() {
    return this._asyncValidatorFns;
  }
  set asyncValidatorFns(value: AsyncValidatorFn[]) {
    this._asyncValidatorFns = value;
    if (this.control) {
      this.updateValidators();
    }
  }

  constructor() {
    super();
  }

  abstract updateValidators(): void;

  getValidatorFn(): ValidatorFn[] {
    return this.validatorFns ?? [];
  }

  getAsyncValidatorFn(): AsyncValidatorFn[] {
    return this.asyncValidatorFns ?? [];
  }
}

@Directive()
export abstract class FormBlockGroup<TD> extends FormBlock<TD, UntypedFormGroup> {
  private _requiredMarks: Array<keyof TD>;

  @Input()
  get requiredMarks() {
    return this._requiredMarks ?? [];
  }
  set requiredMarks(value: Array<keyof TD>) {
    this._requiredMarks = value;
  }

  private _validatorFns: Record<keyof TD, ValidatorFn[]>;

  @Input()
  get validatorFns() {
    return this._validatorFns ?? ({} as Record<keyof TD, ValidatorFn[]>);
  }
  set validatorFns(value: Record<keyof TD, ValidatorFn[]>) {
    this._validatorFns = value;
    if (this.control) {
      this.updateValidators();
    }
  }

  constructor() {
    super();
  }

  abstract updateValidators(): void;

  getValidatorFn(key: keyof TD): ValidatorFn[] {
    return this.validatorFns[key] ?? [];
  }
}
