import { ViewModelBase } from './view-model-base';
import { ServiceBase } from '../data/service-base';
import { EntityManager } from '../data/entity-manager';
import * as breeze from 'breeze-client';
import { Container, observable, computedFrom, inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { CustomLogger } from '../modules/custom-logger';
import { EditingModeEnum } from './editing-mode-enum';
import { DialogOpenResult, DialogCancellableOpenResult, DialogService } from 'aurelia-dialog';
import { ValidationController, ValidateResult, Validator, GlobalValidationConfiguration, PropertyAccessorParser } from 'aurelia-validation';
import { Config } from '../config';
import { ActionDialogBoxInputParameters } from './../model/common-model';
import { KeyboardShortcut } from './../constants/constants';

@inject(Router, CustomLogger, Config)
export abstract class EntityDetailViewModelBase<T extends breeze.Entity> extends ViewModelBase {
  public hasChangesSubscriptionKey: number;
  public entity: T;
  public service: ServiceBase<T>;
  public hasChanges = false;
  public editingMode: EditingModeEnum;
  public get isEditable(): boolean {
    if (this.entity == null) return false;
    if (this.editingMode === EditingModeEnum.Create) return true;
    return (<any>this.entity).isEditable;
  }
  public get canEdit(): boolean {
    if (this.entity == null) return false;
    if (this.editingMode == EditingModeEnum.Create) {
      return false;
    }
    return (<any>this.entity).isLockable && this.canUpdate;
  }
  @computedFrom("isEditable")
  public get disabled(): boolean {
    return !this.isEditable;
  }
  @computedFrom("hasChanges", "isSaving")
  public get canSave(): boolean {
    return this.hasChanges && !this.isSaving;
  }
  @computedFrom("editingMode")
  public get isCreationMode(): boolean {
    return this.editingMode == EditingModeEnum.Create;
  }
  @computedFrom("isCreationMode", "canAdd", "canUpdate")
  public get canAddOrUpdate() {
    if (this.isCreationMode) {
      return this.canAdd;
    }
    return this.canUpdate;
  }

  public abstract get documentTitle(): string;

  public isInitialized = false;
  @observable
  public isSaving = false;
  public entityManager: EntityManager;
  public controller: ValidationController;
  public dialogService: DialogService;
  public dialogRightActions: Array<ActionDialogBoxInputParameters> = [];
  public dialogLeftActions: Array<ActionDialogBoxInputParameters> = [];

  constructor(router: Router, logger: CustomLogger) {
    super(router, logger);
    this.entityManager = Container.instance.get(EntityManager);
    this.hasChanges = this.entityManager.manager.hasChanges();
    this.controller = new ValidationController(
      Container.instance.get(Validator),
      Container.instance.get(PropertyAccessorParser),
      Container.instance.get(GlobalValidationConfiguration),
    );
    this.dialogService = Container.instance.get(DialogService);

    this.hasChangesSubscriptionKey = this.entityManager.manager.hasChangesChanged.subscribe((data) => {
      // console.debug("data has change, " + data.hasChanges);
      this.hasChanges = data.hasChanges;
    });

    this.handleShortcutsEvent = async (event) => await this.handleShortcuts.call(this, event);
    document.addEventListener("keydown", this.handleShortcutsEvent, false);
  }

  public isSavingChanged(newValue, oldValue) {
    this.showLoadingSpinner = (newValue === true);
  }

  public initialize(serviceInstance: ServiceBase<T>) {
    if (serviceInstance != null) {
      this.isInitialized = true;
    }
    this.service = serviceInstance;
  }

  protected getTranslationKeyEntityTypeName(): string {
    var loweredEntityTypeName: string = this.service.entityTypeName.toLocaleLowerCase();
    return loweredEntityTypeName + "." + loweredEntityTypeName;
  }

  @computedFrom("editingMode", "isEditable", "documentTitle", "service")
  public get ribbonHeaderText(): string {
    let docTitle: string = this.documentTitle;
    if (this.editingMode === EditingModeEnum.Update) {
      var translatedEntityTypeName: string = this.i18n.tr(this.getTranslationKeyEntityTypeName());
      if (docTitle == null && this.service != null) {
        docTitle = this.service.getDisplayNameValue(this.entity) ?? '';
      }
      if (this.isEditable)
        return translatedEntityTypeName + " : " + docTitle + " (" + this.i18n.tr("menu.editInProgress") + ")";
      return translatedEntityTypeName + " : " + docTitle;
    }
    return docTitle;
  }

  public cancel(silentCancel: boolean = false, onlyCurrentEntity = false): Promise<any> {
    return this.innerCancel(true, silentCancel, onlyCurrentEntity);
  }

  public innerCancel(allowNavigateBack: boolean = true, silentMode: boolean = false, onlyCurrentEntity: boolean = false) {
    if (this.isSaving) return Promise.reject(false);
    var p = new Promise((resolve, reject) => {
      if (this.hasChanges === true) {
        if (onlyCurrentEntity == true) {
          this.entity.entityAspect.rejectChanges();
          resolve(true);
        }
        else {
          this.service.cancel(silentMode);
          resolve(true);
        }
      }

      this.controller.removeObject(this.entity);
      if (allowNavigateBack && this.editingMode === EditingModeEnum.Create) {
        this.doNavigateBack();
      }
      resolve(true);
    });
    return p;
  }

  public edit(): Promise<any> {
    this.isSaving = true;
    return this.service.lockRecordForEditing(this.entity)
      .catch((errors) => {
        console.log('Rejection of the Promise');
        this.isSaving = false
        return Promise.reject(errors);
      })
      .then(() => this.isSaving = false);
  }

  public refreshData() { }
  public deactivate() {
    this.entityManager.manager.hasChangesChanged.unsubscribe(this.hasChangesSubscriptionKey);
    document.removeEventListener("keydown", this.handleShortcutsEvent, false);
  }

  public canDeactivate() {
    var msg = this.i18n.tr("general.doYouWantToSave", { ns: "common" });
    // var buttonSave = this.i18n.tr("general.yes", { ns: "common" });
    // var buttonIgnore = this.i18n.tr("general.no", { ns: "common" });
    // var buttonCancel = this.i18n.tr("general.close", { ns: "common" });

    let buttonSave: ActionDialogBoxInputParameters = {
      label: this.i18n.tr("general.save", { ns: "common" }),
      title: this.i18n.tr("general.save", { ns: "common" }),
      theme: 'primary',
      type: 'solid',
      disabled: false,
      fn: (thisBox) => { thisBox.controller.ok(true) },
      keyboardShortcut: this.config.globalConfig.keyboardShortcutFwBox ? KeyboardShortcut.Enter : null
    };
    let buttonIgnore: ActionDialogBoxInputParameters = {
      label: this.i18n.tr("general.cancel", { ns: "common" }),
      title: this.i18n.tr("general.cancel", { ns: "common" }),
      theme: 'dark',
      type: 'ghost',
      disabled: false,
      fn: (thisBox) => { thisBox.controller.ok(false) },
      keyboardShortcut: this.config.globalConfig.keyboardShortcutFwBox ? [KeyboardShortcut.Escape, KeyboardShortcut.Backspace] : null
    };


    var p = new Promise<boolean>((resolve, reject) => {
      if (this.isSaving === true) {
        this.logError("Save in progress", "");
        return resolve(false);
      }

      if (!this.hasChanges === true && this.isEditable && this.editingMode !== EditingModeEnum.Create) {
        return this.cancel()
          .then(() => resolve(true))
          .catch(() => resolve(false));
      }
      if (this.hasChanges === true) {
        var asksToSave = () => {
          var confirm = (dor: DialogCancellableOpenResult) => {
            (<DialogOpenResult>dor).closeResult.then((result) => {
              if (!result.wasCancelled) {
                if (result.output === true) {
                  this.innerSave(false)
                    .then(() => {
                      // Save succeed
                      resolve(true);
                    })
                    .catch(() => {
                      // Save failed
                      resolve(false);
                    });
                } else if (result.output === false) {
                  // buttonIgnore -> cancel changes
                  this.innerCancel(false);
                  resolve(true);
                }
              } else {
                // buttonCancel -> do nothing & stay here
                resolve(false);
              }
            });
          };
          this.box.showQuestion(msg, this.config.globalConfig.applicationTitle, [buttonIgnore, buttonSave])
            .then(confirm);
        }
        asksToSave();
      } else {
        resolve(true);
      }
    });
    return p;
  }

  public async navigateBack() {
    let result = await this.canDeactivate();
    if (result) {
      this.doNavigateBack();
    }
  }

  protected doNavigateBack() {
    if ((this.router.history as any).history.length == 1) {
      this.router.navigate('');
    } else {
      this.router.navigateBack();
    }
  }

  protected beforeSave(): Promise<void | boolean> { return new Promise((resolve, reject) => { resolve(true) }); }
  protected afterSave(): Promise<void> { return new Promise((resolve, reject) => { resolve() }); }

  public async saveCurrentEntity(silentSave?: boolean) {
    let beforeSaveResult = await this.beforeSave();
    if (beforeSaveResult == null || beforeSaveResult === true) {
      let validationResult = await this.controller.validate({ object: this.entity });
      if (validationResult.valid === true) {
        this.isSaving = true;
        try {
          await this.service.saveEntity(this.entity, silentSave);
          await this.afterSave();
          this.isSaving = false;
          return this.entity;
        }
        catch (error) {
          this.isSaving = false;
          throw error;
        }
      }
      else {
        validationResult.results.forEach(r => {
          this.logError(r.message, r, (r.message != null));
        });
        throw "There are errors";
      }
    }
  }

  public async save(silentSave: boolean = false, byPassLocking?: boolean, navigateBack: boolean = false): Promise<any> {
    await this.internalSave(silentSave, byPassLocking, navigateBack);
  }
  private async internalSave(silentSave: boolean = false, byPassLocking?: boolean, navigateBack: boolean = false): Promise<any> {
    await this.innerSave(silentSave, byPassLocking)

    if (navigateBack === true) {
      this.doNavigateBack();
    }
    else if (this.editingMode === EditingModeEnum.Create) {
      this.changeRouteAfterCreation();
    }
  }
  protected changeRouteAfterCreation() {
    if (!this.dialogService.hasOpenDialog) {
      let fragment = this.router.currentInstruction.fragment;
      if (fragment == null || fragment == '') {
        fragment = this.router.baseUrl;
      }
      if (this.router.currentInstruction.queryString != null) {
        fragment += '?' + this.router.currentInstruction.queryString;
      }
      let newRoute = fragment.replace('-100', (<any>this.entity).id);
      this.router.navigate(newRoute, { replace: true, trigger: true });
    }
  }

  public checkInitialized() {
    if (!this.isInitialized) {
      this.logError("You must initialize the EntityDetailViewModelBase super object before calling this method", "");
      throw "You must initialize the EntityDetailViewModelBase super object before calling this method";
    }
  }
  private innerSave(silentSave: boolean, byPassLocking?: boolean): Promise<any> {
    this.checkInitialized();
    var self = this;
    var p = new Promise((resolve, reject) => {
      if (this.isSaving === true) {
        self.logError("general.saveInProgress", null, !silentSave, { ns: "common" });
        reject("Save in progress");
        return;
      }

      if (self.hasChanges === false) {
        self.log("general.nothingToSave", null, !silentSave, { ns: "common" });
        reject("Nothing to save");
        return;
      }
      this.beforeSave().then((beforeSaveResult) => {
        if (beforeSaveResult == null || beforeSaveResult === true) {
          const allChangedEntities = this.entityManager.manager.getEntities(null, [breeze.EntityState.Added, breeze.EntityState.Modified]);
          const validateTasks = allChangedEntities.map(e => this.controller.validate({ object: e }));

          Promise.all(validateTasks).then(
            (validationResults) => {
              const notValidResults = validationResults.filter(vr => vr.valid === false);

              if (notValidResults?.length == 0) {

                this.isSaving = true;

                this.service.save(silentSave, byPassLocking)
                  .then(() => {
                    this.afterSave().then(() => {
                      resolve(true)
                    }), (rejectReason) => {
                      this.logError("Error when saving", rejectReason, true);
                      reject(rejectReason);
                    }
                  })
                  .catch((errors) => {
                    reject(errors.message);
                  })
                  .catch(() => console.log('Rejection of the Promise'))
                  .then(() => {
                    this.isSaving = false;
                  });
              } else {
                notValidResults.forEach(validationResult => {
                  validationResult.results.forEach(r => {
                    try {
                      const parsedMessage = JSON.parse(r.message);
                      if (parsedMessage.Parameters) {
                        const newParameters = {};
                        for (const key in parsedMessage.Parameters) {
                          if (parsedMessage.Parameters.hasOwnProperty(key)) {
                            const camelCaseKey = key.charAt(0).toLowerCase() + key.slice(1);
                            newParameters[camelCaseKey] = parsedMessage.Parameters[key];
                          }
                        }
                        parsedMessage.Parameters = newParameters;
                      }

                      this.logError(parsedMessage.ErrorMessageTr, r, (parsedMessage.ErrorMessageTr != null), parsedMessage.Parameters);
                    } catch (e) {
                      this.logError(r.message, r, (r.message != null));
                    }
                  });
                });
                reject("There are errors");

              }
            }, (rejectReason) => {
              this.logError("Error when saving", rejectReason, true);
              reject(rejectReason);
            }
          )
        }
      }), (rejectReason) => {
        this.logError("Error when saving", rejectReason, true);
        reject(rejectReason);
      }
    });
    return p;
  }
  public addNew() {
    let newRoute = this.router.currentInstruction.config.href;
    if (newRoute != null && !Array.isArray(newRoute)) {
      this.router.navigate(newRoute);
    }
  }

  public async delete(detachInverseFKEntities: boolean = true) {
    let isDeleted = await this.service.deleteEntities([this.entity], detachInverseFKEntities);
    if (isDeleted) {
      this.detached();
      this.doNavigateBack();
    }
  }

  private handleShortcutsEvent: (event: KeyboardEvent) => Promise<any>;

  protected async handleShortcuts(event: KeyboardEvent) {
    if (!this.dialogService.hasActiveDialog && (window.navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey) &&
      (event.key == "s" || event.key == "z")) {
      event.stopImmediatePropagation();
      event.preventDefault();
      switch (event.key) {
        case "s":
          if (this.canSave) {
            await this.save();
          }
          break;
        // case "z":
        //   await this.cancel();
        //   break;
      }
      return true;
    }
  }

  public detached() {
    super.detached();
    this.controller.removeObject(this.entity);
  }

  public async selectAction(action: ActionDialogBoxInputParameters) {
    if (action.fn != null) {
      let promise = null;
      if (typeof action.fn === "function") {
        promise = action.fn();
      }
      else if (typeof action.fn === "string" && this[action.fn] != null) {
        promise = this[action.fn]();
      }
      else {
        throw new Error("action.fn must be a function or the name of a function on the dialog's view model");
      }

      if (promise != null) {
        if (typeof promise.then === "function") {
          await (promise as Promise<any>);
        }
      }
    }
  }
}
