import { Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, ViewChild } from "@angular/core";
import { NgModel } from "@angular/forms";
import { BaseComponent } from "../base/base.component";
import { isNumeric, Structure, PabCloisons } from "jalhyd";
import { FormulaireDefinition } from "../../formulaire/definition/form-definition";
import { NgParameter } from "../../formulaire/ngparam";
import { I18nService } from "../../services/internationalisation/internationalisation.service";

/**
 * classe de gestion générique d'un champ de saisie avec titre, validation et message d'erreur
 * définitions :
 * - modèle : entité mémoire gérée, indépendamment de la façon dont elle est affichée.
 *   A noter que si cette entité est une classe, on peut ne présenter à l'interface qu'un membre de cette classe,
 *   cad que get model() et getModelValue() ne renverront pas la même chose.
 *   Par ex : get model()-> instance_de_la_classe_Toto, getModelValue() -> Toto.unMembreNumerique
 * - valeur gérée : entité elle même si c'est un type simple (number, string, ...) ou une partie d'un classe
 * - UI : interface utilisateur, présentation de la valeur gérée
 */
export abstract class GenericInputComponent extends BaseComponent implements OnChanges {
    /**
     * entité mémoire gérée
     */
    protected _model: NgParameter | FormulaireDefinition; // CalcName utilise un FormDefinition ici !?

    /**
     * flag de désactivation de l'input
     */
    @Input()
    private _inputDisabled = false;

    /**
     * flag d'affichage du message d'erreur
     */
    public showError = true;

    /**
     * id de l'input, utilisé notamment pour les tests
     */
    public get inputId() {
        let id = "error-in-inputId";
        if (this._model) {
            // unique input id based on parameter symbol
            if (this._model instanceof NgParameter) {
                const param = this._model as NgParameter;
                id = param.symbol;
                // if inside a nested Structure, prefix with Structure position
                // to disambiguate
                const nub = param.paramDefinition.parentNub;
                if (nub && (nub instanceof Structure || nub instanceof PabCloisons)) {
                    id = nub.findPositionInParent() + "_" + id;
                }
            }
        }
        return id;
    }

    /**
     * chaîne affichée dans l'input quand aucune valeur n'est saisie
     */
    @Input()
    public title: string;

    /**
     * événement signalant un changement : valeur du modèle, validité, ...
     */
    @Output()
    protected change = new EventEmitter<any>();

    /**
     * événement signalant un appui sur TAB ou SHIFT+TAB
     */
    @Output()
    protected tabPressed = new EventEmitter<any>();

    /**
     * valeur saisie.
     * Cette variable n'est modifiée que lorsqu'on affecte le modèle ou que l'utilisateur fait une saisie
     */
    private _uiValue: string;

    /**
     * flag de validité de la saisie dans l'UI
     * par ex : est ce bien une valeur numérique ? n'y a-t-il que des minuscules ? etc...
     */
    private _isValidUI = false;

    /**
     * flag de validité de la valeur du modèle
     * par ex : la valeur saisie fait elle bien partie d'un domaine de définition donné ? date inférieure à une limite ? etc...
     */
    private _isValidModel = false;

    /**
     * message d'erreur UI
     */
    private _errorMessageUI: string;

    /**
     * message d'erreur modèle
     */
    private _errorMessageModel: string;

    @ViewChild("inputControl") inputField: NgModel;

    constructor(private cdRef: ChangeDetectorRef, protected intlService: I18nService) {
        super();
    }

    public get isDisabled(): boolean {
        return this._inputDisabled;
    }

    /**
     * événement de changement de la validité de la saisie
     */
    private emitValidChanged() {
        this.change.emit({ "action": "valid", "value": this.isValid });
    }

    /**
     * détection des changements dans l'UI par le ChangeDetector du framework
     */
    protected detectChanges() {
        if (this.cdRef) {
            // if (!this.cdRef['destroyed']) // pour éviter l'erreur "Attempt to use a destroyed view: detectChanges"
            //     this.cdRef.detectChanges();
            this.cdRef.markForCheck();
        }
    }

    /**
     * calcul de la validité globale du composant (UI+modèle)
     */
    public get isValid() {
        return this._isValidUI && this._isValidModel;
    }

    private setUIValid(b: boolean) {
        const old = this.isValid;
        this._isValidUI = b;
        if (this.isValid !== old) {
            this.emitValidChanged();
        }
    }

    protected validateUI() {
        const { isValid, message } = this.validateUIValue(this._uiValue);
        this._errorMessageUI = message;
        this.detectChanges();
        this.setUIValid(isValid);
        return isValid;
    }

    private setModelValid(b: boolean) {
        const old = this.isValid;
        this._isValidModel = b;
        if (this.isValid !== old) {
            this.emitValidChanged();
        }
        // répercussion des erreurs sur le Form angular, pour faire apparaître/disparaître les mat-error
        if (b) {
            this.inputField.control.setErrors(null);
        } else {
            this.inputField.control.setErrors({ "incorrect": true });
        }
    }

    private validateModel() {
        const { isValid, message } = this.validateModelValue(this.getModelValue());
        this._errorMessageModel = message;
        this.detectChanges();
        this.setModelValid(isValid);
    }

    public validate() {
        this.validateUI();
        this.validateModel();
    }

    /**
     * getter du message d'erreur affiché.
     * L'erreur de forme (UI) est prioritaire
     */
    public get errorMessage() {
        if (this._errorMessageUI !== undefined) {
            return this._errorMessageUI;
        }
        return this._errorMessageModel;
    }

    public get model(): any {
        return this._model;
    }

    /**
     * événement de changement de la valeur du modèle
     */
    private emitModelChanged() {
        this.change.emit({ "action": "model", "value": this.getModelValue() });
    }

    protected setAndValidateModel(sender: any, v: any) {
        this.setModelValue(sender, v);
        this.emitModelChanged();

        this.validateModel();
    }

    public set model(v: any) {
        this.beforeSetModel();
        this._model = v;
        this.afterSetModel();
        this.updateAll();
        this.detectChanges();
    }

    /**
     * MAJ et validation de l'UI
     */
    protected updateAndValidateUI() {
        this._uiValue = String(this.getModelValue());
        this.validateUI();
    }

    public get uiValue() {
        return this._uiValue;
    }

    /*
     * fonction appelée lorsque l'utilisateur fait une saisie
     * @param ui valeur dans le contrôle
     */
    public set uiValue(ui: any) {
        this._uiValue = ui;
        this.updateModelFromUI();
    }

    /**
     * met à jour le modèle d'après la saisie
     */
    public updateModelFromUI() {
        if (this.validateUI()) {
            this.setAndValidateModel(this, +this._uiValue); // cast UI value to Number
        }
    }

    /**
     * Renvoie l'événement au composant du dessus
     */
    public onTabPressed(event, shift: boolean) {
        this.tabPressed.emit({ originalEvent: event, shift: shift });
        return false; // stops event propagation
    }

    private updateAll() {
        this.updateAndValidateUI();
        this.validateModel();
    }

    /**
     * appelé quand les @Input changent
     */
    public ngOnChanges() {
        this.updateAll();
    }

    /**
     * appelé avant le changement de modèle
     */
    protected beforeSetModel() { }

    /**
     * appelé après le changement de modèle
     */
    protected afterSetModel() { }

    /**
     * retourne la valeur du modèle
     */
    protected abstract getModelValue(): any;

    /**
     * affecte la valeur du modèle
     */
    protected abstract setModelValue(sender: any, v: any);

    /**
     * valide une valeur de modèle : est ce une valeur acceptable ? (par ex, nombre dans un intervalle, valeur dans une liste, ...)
     * @param v valeur à valider
     * @returns isValid : true si la valeur est valide, false sinon
     * @returns message : message d'erreur
     */
    protected abstract validateModelValue(v: any): { isValid: boolean, message: string };

    /**
     * valide une valeur saisie dans l'UI (forme de la saisie : est ce bien une date, un nombre, ...)
     * @param ui saisie à valider
     * @returns isValid : true si la valeur est valide, false sinon
     * @returns message : message d'erreur
     */
    protected validateUIValue(ui: string): { isValid: boolean, message: string } {
        let valid = false;
        let msg: string;

        if (! isNumeric(ui)) {
            msg = this.intlService.localizeText("ERROR_PARAM_MUST_BE_A_NUMBER");
        } else {
            valid = true;
        }

        return { isValid: valid, message: msg };
    }
}