/*
 * Copyright (c) 2010, 2023 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {arrays, DefaultStatus, EnumObject, FullModelOf, InitModelOf, NotificationBadgeStatus, ObjectOrModel, objects, ObjectWithType, ParsingFailedStatus, Predicate, scout, StatusModel, strings, ValidationFailedStatus} from '../index';
import $ from 'jquery';

export class Status implements StatusModel, ObjectWithType {
  declare model: StatusModel;

  objectType: string;
  message: string;
  severity: StatusSeverity;
  iconId: string;
  code: number;
  children: Status[];
  deletable: boolean;
  uiState: string;

  constructor(model?: InitModelOf<Status>) {
    this.message = null;
    this.severity = Status.Severity.ERROR;
    this.iconId = null;
    this.code = 0;
    this.children = null;
    this.deletable = true;
    $.extend(this, model);

    // severity may be a string (e.g. if set in a model json file) -> convert to real severity
    if (typeof this.severity === 'string') {
      let currentSeverity = this.severity as string;
      this.severity = Status.Severity[currentSeverity.toUpperCase()];
    }
    // children
    if (model && model.children && Array.isArray(model.children)) {
      this.children = model.children.map(child => Status.ensure(child));
    }
  }

  static Severity = {
    OK: 0x01,
    INFO: 0x100,
    WARNING: 0x10000,
    ERROR: 0x1000000
  } as const;

  static SEVERITY_CSS_CLASSES = 'error warning info ok';

  cssClass(): string {
    return Status.cssClassForSeverity(this.severity);
  }

  /**
   * @returns true if severity is OK or INFO, false if severity is WARNING or ERROR.
   */
  isValid(): boolean {
    return this.severity === Status.Severity.OK ||
      this.severity === Status.Severity.INFO;
  }

  isError(): boolean {
    return this.severity === Status.Severity.ERROR;
  }

  isWarning(): boolean {
    return this.severity === Status.Severity.WARNING;
  }

  isInfo(): boolean {
    return this.severity === Status.Severity.INFO;
  }

  isOk(): boolean {
    return this.severity === Status.Severity.OK;
  }

  /**
   * @returns status including all children recursively as flat array.
   */
  asFlatList(): Status[] {
    return Status.asFlatList(this);
  }

  /**
   * @returns a clone of this Status instance.
   */
  clone(): Status {
    let modelClone = $.extend({}, this);
    return new Status(modelClone);
  }

  equals(o: any): boolean {
    if (!(o instanceof Status)) {
      return false;
    }
    if (!objects.equalsRecursive(this.children, o.children)) {
      return false;
    }
    return objects.propertiesEquals(this, o, ['severity', 'message', 'invalidDate', 'invalidTime']);
  }

  /**
   * Note: we cannot 'overload' this function, because predicates and status-types are both functions,
   * thus we cannot distinguish them by type or instanceof.
   *
   * @returns whether or not this status contains a child with the give type
   */
  containsStatus(statusType: abstract new() => Status): boolean {
    return this.containsStatusByPredicate(status => status instanceof statusType);
  }

  containsStatusByPredicate(predicate: Predicate<Status>): boolean {
    return this.asFlatList().some(predicate);
  }

  addStatus(status: Status) {
    if (this.hasChildren()) {
      this.children.push(status);
    } else {
      this.children = [status];
    }
    this._updateProperties();
  }

  /**
   * Removes all children of the given type from this status. The type is checked by inheritance.
   */
  removeAllStatus(statusType: abstract new() => Status) {
    this.removeAllStatusByPredicate(status => status instanceof statusType);
  }

  removeAllStatusByPredicate(predicate: Predicate<Status>) {
    if (this.hasChildren()) {
      this.children.forEach(status => {
        status.removeAllStatusByPredicate(predicate);
      });
      this.children = this.children.filter(status => {
        // when status is not deletable we must add it as child again, thus --> true
        if (!status.deletable) {
          return true;
        }
        return !predicate(status); // negate predicate
      });
      this._updateProperties();
    }
  }

  protected _updateProperties() {
    if (!this.hasChildren()) {
      this.message = null;
      this.severity = Status.Severity.OK;
      this.code = 0;
      return;
    }

    let firstStatus = this.asFlatList().sort((a, b) => {
      return calcPriority(b) - calcPriority(a);

      function calcPriority(status) {
        let multiplier = 1;
        if (status instanceof ParsingFailedStatus) {
          multiplier = 4;
        } else if (status instanceof ValidationFailedStatus) {
          multiplier = 2;
        }
        return multiplier * status.severity;
      }
    })[0];
    this.message = firstStatus.message;
    this.severity = firstStatus.severity;
    this.code = firstStatus.code;
  }

  /**
   * @returns whether this status has children (= multi status)
   */
  hasChildren(): boolean {
    return !!(this.children && this.children.length > 0);
  }

  /**
   * In some cases we need to transform an error status without children to a multi-status with children.
   * @returns If this instance already has children, a clone of the instance. Otherwise, a new instance with the current instance as first child.
   */
  ensureChildren(): Status {
    if (objects.isArray(this.children)) {
      return this.clone();
    }
    let childStatus = this;
    let newStatus = this.clone();
    newStatus.children = [childStatus];
    newStatus._updateProperties();
    return newStatus;
  }

  /* --- STATIC HELPERS ------------------------------------------------------------- */

  /**
   * Null-safe static clone method.
   */
  static clone(original: Status): Status {
    return original ? original.clone() : null;
  }

  static cssClassForSeverity(severity: StatusSeverity): string {
    let cssSeverity,
      Severity = Status.Severity;

    switch (severity) {
      case Severity.OK:
        cssSeverity = 'ok';
        break;
      case Severity.INFO:
        cssSeverity = 'info';
        break;
      case Severity.WARNING:
        cssSeverity = 'warning';
        break;
      case Severity.ERROR:
        cssSeverity = 'error';
        break;
    }
    return cssSeverity;
  }

  static animateStatusMessage($status: JQuery, message: string) {
    if (strings.endsWith(message, '...')) {
      let $ellipsis = $status.makeSpan('ellipsis');
      for (let i = 0; i < 3; i++) {
        $ellipsis.append($status.makeSpan('animate-dot delay-' + i, '.'));
      }
      message = message.substring(0, message.length - 3);
      $status.empty().text(message).append($ellipsis);
    } else {
      $status.text(message);
    }
  }

  /**
   * @returns a {@link Status} object with severity OK.
   */
  static ok(model?: StatusModel | string): Status {
    return new Status(Status.ensureModel(model, Status.Severity.OK));
  }

  /**
   * @returns a {@link Status} object with severity INFO.
   */
  static info(model?: StatusModel | string): Status {
    return new Status(Status.ensureModel(model, Status.Severity.INFO));
  }

  /**
   * @returns a {@link Status} object with severity WARNING.
   */
  static warning(model?: StatusModel | string): Status {
    return new Status(Status.ensureModel(model, Status.Severity.WARNING));
  }

  /**
   * @returns a {@link Status} object with severity ERROR.
   */
  static error(model?: StatusModel | string): Status {
    return new Status(Status.ensureModel(model, Status.Severity.ERROR));
  }

  static ensureModel(model: StatusModel | string, severity: StatusSeverity | StatusSeverityNames): StatusModel {
    if (typeof model === 'string') {
      model = {
        message: model
      };
    } else {
      model = model || {};
    }
    return $.extend({}, model, {
      severity: severity
    });
  }

  /**
   * @returns all {@link  Status} objects as flat array (recursively goes through the status hierarchy)
   */
  static asFlatList(status: Status): Status[] {
    if (!status) {
      return [];
    }
    let list = [];
    if (status.hasChildren()) {
      status.children.forEach(childStatus => {
        arrays.pushAll(list, Status.asFlatList(childStatus));
      });
      list.sort((s1, s2) => {
        if (s1 instanceof NotificationBadgeStatus) {
          if (s2 instanceof NotificationBadgeStatus) {
            return 0;
          }
          return -1;
        }
        if (s2 instanceof NotificationBadgeStatus) {
          return 1;
        }
        return 0;
      });
    } else {
      list.push(status);
    }
    return list;
  }

  /**
   * Returns a constructor function for the given class-name.
   *
   * @returns Status constructor
   */
  static classForName(className: StatusType): new(model?: StatusModel) => Status {
    return {
      Status: Status,
      DefaultStatus: DefaultStatus,
      NotificationBadgeStatus: NotificationBadgeStatus,
      ParsingFailedStatus: ParsingFailedStatus,
      ValidationFailedStatus: ValidationFailedStatus
    }[className];
  }

  static ensure<T extends Status = Status>(status: StatusOrModel<T>): T {
    if (!status) {
      return status as T;
    }
    if (status instanceof Status) {
      return status;
    }
    // May return a specialized subclass of Status
    if (!status.objectType) {
      status.objectType = Status;
    }
    return scout.create(status as FullModelOf<T>);
  }
}

export type StatusSeverity = EnumObject<typeof Status.Severity>;
export type StatusSeverityNames = keyof typeof Status.Severity;
export type StatusOrModel<T extends Status = Status> = ObjectOrModel<T>;
export type StatusType = 'Status' | 'DefaultStatus' | 'ParsingFailedStatus' | 'ValidationFailedStatus';
