import { AfterViewInit, Component, ContentChild, ElementRef, EventEmitter, Injector, Input, OnChanges, Output, SimpleChanges, ViewChild, ViewChildren } from '@angular/core';

import { HypermediaAction, HypermediaField, LabelingService } from 'first-npm-package-nicule/core';
import { NgForm } from '@angular/forms';
import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { FormFieldsComponent } from '../form-fields.component';
import { PARENT_FORM } from '../parent-form';
import { FormFieldComponent } from '../form-field.component';

@Component({
    selector: 'hm-form',
    templateUrl: 'form.component.html',
    styleUrls: ['form.component.scss'],
    providers: [{provide: PARENT_FORM, useExisting: HypermediaForm}],
    viewProviders: [{provide: PARENT_FORM, useExisting: HypermediaForm}],
    exportAs: 'hmForm'
})
export class HypermediaForm implements AfterViewInit, OnChanges {
    @Input() action: HypermediaAction;
    @Input() tokenOverride: string;
    @Input() preventSubmissionOnEnter = true;
    @Input() disabled = false; // this overwrites all inputs
    @Input() mode: 'regular' | 'narrow' | 'wide' = 'regular'; // this overwrites all inputs
    @Input() additionalData: any;

    @Output() actionSuccess = new EventEmitter();
    @Output() actionError = new EventEmitter();
    @Output() init = new EventEmitter();
    @Output() valueChanged = new EventEmitter();
    @Output() actionSubmit = new EventEmitter();
    @Output() actionComplete = new EventEmitter();
    @Output() escapeKeydown = new EventEmitter();

    @ViewChild('actionForm', { static: false }) ngForm: NgForm;
    @ViewChild('actionForm', {read: ElementRef, static: false }) formElement: ElementRef;
    @ViewChildren('fallbackField', {read: FormFieldComponent }) fallbackFields: FormFieldComponent;

    _formFields: FormFieldsComponent;
    get formFields(): FormFieldsComponent {
        return this._formFields;
    }

    @ContentChild(FormFieldsComponent, { static: false }) set formFields(newValue: FormFieldsComponent) {
        this._formFields = newValue;
    }

    isSubmiting = false;
    error: string;

    fields: Array<HypermediaField & { fieldInputs?: any }> = [];

    get value(): any {
        return this.ngForm && this.ngForm.value || {};
    }

    get isValid(): boolean {
        return this.ngForm && this.ngForm.valid;
    }

    private valueChangeSubscription: Subscription;

    ngOnChanges(changes: SimpleChanges): void {
        if ('action' in changes && this.action) {
            const {action} = this as any;

            this.fields = action && this.cloneFields(action.fields) || []; // this should be made in a imutable
        }
    }

    cloneFields(source: Array<HypermediaField>): Array<HypermediaField> {
        return source.map(({fields, options, flex, ...rest}) => ({
            ...rest,
            fields: fields && fields.map(field => ({...field})),
            options: options && options.map(option => ({...option})),
            flex: flex && Array.isArray(flex) ? flex.map(flexItem => flexItem) : flex
        }));
    }

    constructor(
        private translate: TranslateService,
        private labelingService: LabelingService,
        private _injector: Injector
    ) {
    }

    submit(formValues?: any): void {
        if (this.isSubmiting === true) {
            return;
        }

        this.isSubmiting = true;

        if (formValues && Object.keys(this.ngForm.form.controls).length > 0) {
            this.ngForm.setValue({...this.ngForm.value, ...formValues});
        }

        this.ngForm.onSubmit(new MouseEvent(''));
    }

    onSuccess(event = {} as any): void {
        this.isSubmiting = false;
        this.actionSuccess.emit(event);
    }

    onError(event = {} as any): void {
        this.isSubmiting = false;
        if (typeof event === 'string') {
            this.error = event;
        }
        this.actionError.emit(event);
    }

    onComplete(): void {
        this.isSubmiting = false;
        this.actionComplete.emit();

        const errors = [];
        Object.keys(this.ngForm.controls)
            .forEach(key => {
                const currentField = this.ngForm.controls[key];

                if (!currentField.valid) {
                    errors.push(key);
                }
            });

        if (errors.length) {
            this.scrollToError(errors);
        }
    }

    formSubmission(event): void {
        this.actionSubmit.emit();
        if (!this.preventSubmissionOnEnter) {
            return;
        }

        event.stopPropagation(); // stop the event from propagating on the form, preventing submit
    }

    showErrors(onlyPrefilledFields = false): void {
        const errors = [];
        Object.keys(this.ngForm.controls)
            .filter(key => !onlyPrefilledFields || this.ngForm.controls[key].value)
            .forEach(key => {
                const currentField = this.ngForm.controls[key];
                this.ngForm.controls[key].markAsDirty();
                this.ngForm.controls[key].markAsTouched();

                if (!currentField.valid) {
                    errors.push(key);
                }
            });

        if (errors.length) {
            this.scrollToError(errors);
        }
    }

    scrollToError(errors): void {
        if (this.formFields && this.formFields.orderer) {
            for (const formFieldComponent of this.formFields.orderer.ordered) {
                let name = formFieldComponent.named;
                const type = formFieldComponent.baseField.type;
                let selector;
                let foundInGroup = false;

                if (type === 'group') {
                    const fieldsInGroup = formFieldComponent.baseField.fields.map(i => i.name);
                    foundInGroup = errors.some(r => {
                        if (fieldsInGroup.indexOf(r) >= 0) {
                            name = r;

                            return true;
                        }

                        return false;
                    });
                }

                if (errors.includes(name) || foundInGroup) {
                    switch (type) {
                        case 'boolean':
                            selector = `[name=${name}]`;
                            break;
                        case 'options':
                            selector = `mat-select[id^=${name}-]`;
                            break;
                        default:
                            selector = `[id^=form-field-${name}-]`;
                    }

                    const elem = this.formElement.nativeElement.querySelector(selector);
                    if (elem) {
                        elem.scrollIntoView({behavior: 'smooth', block: 'center'});
                    }

                    break;
                }
            }
        }
    }

    reset(resetForm = true): any {
        this.error = '';
        if (!this.ngForm) {
            return;
        }

        if (resetForm) {
            this.ngForm.reset();
        }
        this.ngForm.control.setErrors({}, {emitEvent: true});

        this.reinitializeFields(this.action.fields);
    }

    triggerSubmit(): void {
        this.actionSubmit.emit();
    }

    private reinitializeFields(fields: Array<HypermediaField> = []): void {
        fields.forEach(field => {
            if (field.type === 'group') {
                this.reinitializeFields(field.fields);
            } else {
                this.reinitializeField(field);
            }
        });
    }

    private reinitializeField({name, value}: HypermediaField): void {
        const control = this.ngForm.controls[name];

        if (control) {
            control.setValue(value);
        }
    }

    setValue(value: any): any {
        if (!value) {
            return;
        }

        Object
            .keys(value)
            .forEach(name => {
                const control = this.ngForm.controls[name];

                if (control) {
                    control.setValue(value[name]);
                }
            });
    }

    ngDestroy(): void {
        this.valueChangeSubscription.unsubscribe();
    }

    ngAfterViewInit(): void {
        const form = this.ngForm;

        if (form !== undefined) {
            this.init.emit();

            this.valueChangeSubscription = form.valueChanges.subscribe(newValue => {
                if (this.action && this.action['$$validator']) {
                    const validationResponse = this.action['$$validator'](newValue);

                    if (validationResponse) {
                        const propertyKeys = Object.keys(validationResponse);

                        propertyKeys.forEach(propertyKey => {
                            const control = form.controls[propertyKey];

                            if (control) {
                                const modelErrors = validationResponse[propertyKey];
                                const defaultErrors = control.validator(control);
                                const combinedErrors = {...defaultErrors, ...modelErrors};
                                const hasErrors = Object.keys(combinedErrors).length > 0;

                                if (hasErrors) {
                                    control.setErrors(combinedErrors);
                                } else {
                                    control.setErrors(undefined);
                                }
                            }
                        });
                    }
                }

                this.valueChanged.emit(newValue);
            });
        }
    }

    getField(named: string): any {
        return this.fields.find(({name}) => name === named);
    }

    getSelectedOptionName(fieldName: string): string {
        const field = this.fields
            .find(f => f.name === fieldName);

        return field.options
            .find(option => option.value === field.value)
            .name;
    }
}
