import { Input, Output, EventEmitter, ChangeDetectorRef } from "@angular/core";

import { BaseComponent } from "../base/base.component";

/*
exemple de template :

<div class="md-form form-sm">
    <input mdbActive type="text" id="form1" class="form-control" [disabled]="isDisabled" [ngModel]="uiValue" (ngModelChange)="setUIValue($event)">
    <label for="form1">{{_title}}</label>
    <small class="text-danger">{{_message}}</small>
</div>
*/

/**
 * 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 {
    /**
     * entité mémoire gérée
     */
    protected _model: any;

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

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

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

    /**
     * événement signalant un changement : valeur du modèle, validité, ...
     */
    @Output()
    protected onChange = 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;

    constructor(private cdRef: ChangeDetectorRef) {
        super();
    }

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

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

    /**
     * détection des changements dans l'UI par le ChangeDetector du framework
     */
    protected detectChanges() {
        if (this.cdRef != undefined)
            // 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();
    }

    private validateUI() {
        let { 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();
    }

    private validateModel() {
        let { 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
     */
    private 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.onChange.emit({ "action": "model", "value": this.getModelValue() });
    }

    private 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 = this.modelToUI(this.getModelValue());
        this.validateUI();
    }

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

    /*
     * fonction appelée lorsque l'utilisateur fait une saisie
     * @param ui valeur dans le contrôle
     */
    private 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.uiToModel(this._uiValue));
    }

    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 };

    /**
     * convertit le modèle en valeur affichable par l'UI
     */
    protected abstract modelToUI(v: any): 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 abstract validateUIValue(ui: string): { isValid: boolean, message: string };

    /**
     * convertit une valeur saisie dans l'UI en valeur affectable au modèle
     */
    protected abstract uiToModel(ui: string): any;
}

/*
 * exemples d'utilisation de GenericInputComponent
 */

/*
import { Component } from "@angular/core";

import { NumericalString, Message } from "jalhyd";

// exemple où le modèle est un type simple (number)

@Component({
    selector: "test-input",
    template: `<div class="md-form form-sm">
    <input mdbActive type="text" id="form1" class="form-control" [disabled]="isDisabled" [(ngModel)]="uiValue">
    <label for="form1">{{_title}}</label>
    <small *ngIf="showError" class="text-danger">{{errorMessage}}</small>
</div>`
})
export class TestInputComponent extends GenericInputComponent {
    constructor() {
        super();
        this._model = 0;
    }

    protected getModelValue(): any {
        return this._model;
    }

    protected setModelValue(v: any) {
        this._model = v;
    }

    protected validateModelValue(v: any): { isValid: boolean, message: string } {
        let msg = undefined;
        let valid = false;

        if (v < 0)
            msg = "La valeur n'est pas >= 0 ";
        else
            valid = true;

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

    protected modelToUI(v: any): string {
        if (typeof (v) == "number")
            return String(v);
        return undefined;
    }

    protected validateUIValue(ui: string): { isValid: boolean, message: string } {
        let valid: boolean = false;
        let msg: string = undefined;

        let v: NumericalString = new NumericalString(ui);
        if (!v.isNumerical)
            msg = "Veuillez entrer une valeur numérique";
        else
            valid = true;

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

    protected uiToModel(ui: string): any {
        return +ui;
    }
}


// exemple où le modèle est une classe dont on ne gère qu'un membre

import { BaseParam } from "jalhyd";

@Component({
    selector: "test2-input",
    template: `<div class="md-form form-sm">
    <input mdbActive type="text" id="form1" class="form-control" [disabled]="isDisabled" [(ngModel)]="uiValue">
    <label for="form1">{{_title}}</label>
    <small *ngIf="showError" class="text-danger">{{errorMessage}}</small>
</div>`
})
export class Test2InputComponent extends GenericInputComponent {
    constructor() {
        super();
    }

    // paramètre géré
    private get _param(): BaseParam {
        return this._model;
    }

    protected getModelValue(): any {
        return this._param.getValue();
    }

    protected setModelValue(v: any) {
        this._param.setValue(v);
    }

    protected validateModelValue(v: any): { isValid: boolean, message: string } {
        let msg = undefined;
        let valid = false;

        if (v < 0)
            msg = "La valeur n'est pas >= 0 ";
        else
            valid = true;

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

    protected modelToUI(v: any): string {
        if (typeof (v) == "number")
            return String(v);
        return undefined;
    }

    protected validateUIValue(ui: string): { isValid: boolean, message: string } {
        let valid: boolean = false;
        let msg: string = undefined;

        let v: NumericalString = new NumericalString(ui);
        if (!v.isNumerical)
            msg = "Veuillez entrer une valeur numérique";
        else
            valid = true;

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

    protected uiToModel(ui: string): any {
        return +ui;
    }
}
/**/