// eslint-disable-next-line max-classes-per-file
import { ElementComponent } from 'common/directives/element-component.directive';
import { Type } from '@angular/core';
import { ClozeDocument } from 'common/models/elements/compound-elements/cloze/cloze';
import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row';

import { Label, TextLabel } from 'common/models/elements/label-interfaces';
import { Hotspot } from 'common/models/elements/input-elements/hotspot-image';
import {
  DimensionProperties,
  PlayerProperties,
  PositionProperties,
  PropertyGroupGenerators,
  PropertyGroupValidators,
  Stylings
} from 'common/models/elements/property-group-interfaces';
import { VisibilityRule } from 'common/models/visibility-rule';
import { StateVariable } from 'common/models/state-variable';
import { environment } from 'common/environment';
import { InstantiationEror } from 'common/util/errors';

import { MathTableRow } from 'common/models/elements/input-elements/math-table';
import { VariableInfo } from '@iqb/responses';
import { Markable } from 'player/src/app/models/markable.interface';

export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simple' | 'text-area' | 'checkbox'
| 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert-row' | 'radio-group-images' | 'hotspot-image'
| 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button' | 'geometry'
| 'math-field' | 'math-table' | 'text-area-math' | 'trigger' | 'table' | 'remote-control' ;

export interface OptionElement extends UIElement {
  getNewOptionLabel(optionText: string): Label;
}

export interface Measurement {
  value: number;
  unit: string
}

export interface ValueChangeElement {
  id: string;
  value: InputElementValue;
}

export type UIElementValue = string | number | boolean | undefined | UIElementType | InputElementValue |
TextLabel | TextLabel[] | ClozeDocument | LikertRowElement[] | Hotspot[] | StateVariable |
PositionProperties | PlayerProperties | Measurement | Measurement[] | VisibilityRule[];

export type InputAssistancePreset = null | 'french' | 'numbers' | 'numbersAndOperators' | 'numbersAndBasicOperators'
| 'comparisonOperators' | 'squareDashDot' | 'placeValue' | 'space' | 'comma' | 'custom';

export interface UIElementProperties {
  id: string;
  isRelevantForPresentationComplete: boolean;
  dimensions?: DimensionProperties;
  position?: PositionProperties;
  styling?: Stylings;
  player?: PlayerProperties;
}

function isValidUIElementProperties(blueprint?: UIElementProperties): boolean {
  if (!blueprint) return false;
  return blueprint.id !== undefined &&
    blueprint.isRelevantForPresentationComplete !== undefined;
}

export abstract class UIElement implements UIElementProperties {
  [index: string]: unknown;
  id: string = 'id-placeholder';
  isRelevantForPresentationComplete: boolean = true;
  abstract type: UIElementType;
  position?: PositionProperties;
  dimensions?: DimensionProperties;
  styling?: Stylings;
  player?: PlayerProperties;

  constructor(element?: UIElementProperties) {
    if (element && isValidUIElementProperties(element)) {
      this.id = element.id;
      this.isRelevantForPresentationComplete = element.isRelevantForPresentationComplete;
      if (element.dimensions) this.dimensions = { ...element.dimensions };
      if (element.position) this.position = { ...element.position };
      if (element.styling) this.styling = { ...element.styling };
    } else {
      if (environment.strictInstantiation) {
        throw new InstantiationEror('Error at UIElement instantiation', element);
      }
      if (element?.id) this.id = element.id;
      if (element?.isRelevantForPresentationComplete !== undefined) {
        this.isRelevantForPresentationComplete = element.isRelevantForPresentationComplete;
      }
      this.position = PropertyGroupGenerators.generatePositionProps(element?.position);
      this.dimensions = PropertyGroupGenerators.generateDimensionProps(element?.dimensions);
    }
  }

  setProperty(property: string, value: unknown): void {
    if (Array.isArray(this[property])) { // keep array reference intact
      (this[property] as UIElementValue[])
        .splice(0, (this[property] as UIElementValue[]).length, ...(value as UIElementValue[]));
    } else {
      this[property] = value;
    }
  }

  setStyleProperty(property: string, value: UIElementValue): void {
    (this.styling as Stylings)[property] = value;
  }

  setPositionProperty(property: string, value: UIElementValue): void {
    (this.position as PositionProperties)[property] = value;
  }

  setDimensionsProperty(property: string, value: number | null): void {
    (this.dimensions as DimensionProperties)[property] = value;
  }

  setPlayerProperty(property: string, value: UIElementValue): void {
    (this.player as PlayerProperties)[property] = value;
  }

  // eslint-disable-next-line class-methods-use-this
  getChildElements(): UIElement[] {
    return [];
  }

  // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
  getVariableInfos(options?: unknown): VariableInfo[] {
    return [];
  }

  abstract getElementComponent(): Type<ElementComponent>;

  static createOptionLabel(optionText: string, addImg: boolean = false) {
    return {
      text: optionText,
      imgSrc: addImg ? null : undefined,
      imgPosition: addImg ? 'above' : undefined
    };
  }

  abstract getDuplicate(): UIElement;
}

export type InputElementValue = Markable[] | TextLabel[] | Hotspot[] | MathTableRow[] | GeometryValue | string[] | string |
number[] | number | boolean[] | boolean | null;

export interface InputElementProperties extends UIElementProperties {
  label?: string;
  value: InputElementValue;
  required: boolean;
  requiredWarnMessage: string;
  readOnly: boolean;
}

function isValidInputElementProperties(blueprint?: InputElementProperties): boolean {
  if (!blueprint) return false;
  return blueprint?.value !== undefined &&
    blueprint?.required !== undefined &&
    blueprint?.requiredWarnMessage !== undefined &&
    blueprint?.readOnly !== undefined;
}

export abstract class InputElement extends UIElement implements InputElementProperties {
  label?: string = 'Beschriftung';
  value: InputElementValue = null;
  required: boolean = false;
  requiredWarnMessage: string = 'Eingabe erforderlich';
  readOnly: boolean = false;

  protected constructor(element?: InputElementProperties) {
    super(element);
    if (element && isValidInputElementProperties(element)) {
      if (element.label !== undefined) this.label = element.label;
      this.value = element.value;
      this.required = element.required;
      this.requiredWarnMessage = element.requiredWarnMessage;
      this.readOnly = element.readOnly;
    } else {
      if (environment.strictInstantiation) {
        throw new InstantiationEror('Error at InputElement instantiation', element);
      }
      if (element?.label !== undefined) this.label = element.label;
      if (element?.value !== undefined) this.value = element.value;
      if (element?.required !== undefined) this.required = element.required;
      if (element?.requiredWarnMessage !== undefined) this.requiredWarnMessage = element.requiredWarnMessage;
      if (element?.readOnly !== undefined) this.readOnly = element.readOnly;
    }
  }

  static stripHTML(htmlString: string): string {
    const parser = new DOMParser();
    const htmlDocument = parser.parseFromString(htmlString, 'text/html');
    return htmlDocument.documentElement.textContent || '';
  }
}

export function isInputElement(el: UIElement): el is InputElement {
  return el.value !== undefined &&
    el.required !== undefined &&
    el.requiredWarnMessage !== undefined &&
    el.readOnly !== undefined;
}

export interface KeyInputElementProperties {
  inputAssistancePreset: InputAssistancePreset;
  inputAssistancePosition: 'floating' | 'right';
  inputAssistanceFloatingStartPosition: 'startBottom' | 'endCenter';
  showSoftwareKeyboard: boolean;
  addInputAssistanceToKeyboard: boolean;
  hideNativeKeyboard: boolean;
  hasArrowKeys: boolean;
}

export interface TextInputElementProperties extends KeyInputElementProperties, InputElementProperties {
  inputAssistanceCustomKeys: string;
  restrictedToInputAssistanceChars: boolean;
  hasBackspaceKey: boolean;
}

function isValidKeyInputProperties(blueprint?: KeyInputElementProperties): boolean {
  if (!blueprint) return false;
  return blueprint.inputAssistancePreset !== undefined &&
    blueprint.inputAssistancePosition !== undefined &&
    blueprint.inputAssistanceFloatingStartPosition !== undefined &&
    blueprint.showSoftwareKeyboard !== undefined &&
    blueprint.addInputAssistanceToKeyboard !== undefined &&
    blueprint.hideNativeKeyboard !== undefined &&
    blueprint.hasArrowKeys !== undefined;
}

function isValidTextInputElementProperties(blueprint?: TextInputElementProperties): boolean {
  if (!blueprint) return false;
  return blueprint.restrictedToInputAssistanceChars !== undefined &&
    blueprint.inputAssistanceCustomKeys !== undefined &&
    blueprint.hasBackspaceKey !== undefined &&
    isValidKeyInputProperties(blueprint);
}

export abstract class TextInputElement extends InputElement implements TextInputElementProperties {
  inputAssistancePreset: InputAssistancePreset = null;
  inputAssistanceCustomKeys: string = '';
  inputAssistancePosition: 'floating' | 'right' = 'floating';
  inputAssistanceFloatingStartPosition: 'startBottom' | 'endCenter' = 'startBottom';
  restrictedToInputAssistanceChars: boolean = false;
  hasArrowKeys: boolean = false;
  hasBackspaceKey: boolean = false;
  showSoftwareKeyboard: boolean = true;
  addInputAssistanceToKeyboard: boolean = true;
  hideNativeKeyboard: boolean = true;

  protected constructor(element?: TextInputElementProperties) {
    super(element);
    if (element && isValidTextInputElementProperties(element)) {
      this.inputAssistancePreset = element.inputAssistancePreset;
      this.inputAssistanceCustomKeys = element.inputAssistanceCustomKeys;
      this.inputAssistancePosition = element.inputAssistancePosition;
      this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition;
      this.restrictedToInputAssistanceChars = element.restrictedToInputAssistanceChars;
      this.hasArrowKeys = element.hasArrowKeys;
      this.hasBackspaceKey = element.hasBackspaceKey;
      this.showSoftwareKeyboard = element.showSoftwareKeyboard;
      this.hideNativeKeyboard = element.hideNativeKeyboard;
      this.addInputAssistanceToKeyboard = element.addInputAssistanceToKeyboard;
    } else {
      if (environment.strictInstantiation) {
        throw Error('Error at TextInputElement instantiation');
      }
      if (element?.inputAssistancePreset) this.inputAssistancePreset = element.inputAssistancePreset;
      if (element?.inputAssistanceCustomKeys) this.inputAssistanceCustomKeys = element.inputAssistanceCustomKeys;
      if (element?.inputAssistancePosition) this.inputAssistancePosition = element.inputAssistancePosition;
      if (element?.inputAssistanceFloatingStartPosition) this.inputAssistanceFloatingStartPosition = element.inputAssistanceFloatingStartPosition;
      if (element?.restrictedToInputAssistanceChars) this.restrictedToInputAssistanceChars = element.restrictedToInputAssistanceChars;
      if (element?.hasArrowKeys) this.hasArrowKeys = element.hasArrowKeys;
      if (element?.hasBackspaceKey) this.hasBackspaceKey = element.hasBackspaceKey;
      if (element?.showSoftwareKeyboard) this.showSoftwareKeyboard = element.showSoftwareKeyboard;
      if (element?.addInputAssistanceToKeyboard) this.addInputAssistanceToKeyboard = element.addInputAssistanceToKeyboard;
      if (element?.hideNativeKeyboard) this.hideNativeKeyboard = element.hideNativeKeyboard;
    }
  }
}

export abstract class CompoundElement extends UIElement {
  abstract getChildElements(): UIElement[];
}

export interface PlayerElementBlueprint extends UIElementProperties {
  player: PlayerProperties;
}

function isValidPlayerElementBlueprint(blueprint?: PlayerElementBlueprint): boolean {
  if (!blueprint) return false;
  return blueprint.player !== undefined &&
    blueprint.player.autostart !== undefined &&
    blueprint.player.autostartDelay !== undefined &&
    blueprint.player.loop !== undefined &&
    blueprint.player.startControl !== undefined &&
    blueprint.player.pauseControl !== undefined &&
    blueprint.player.progressBar !== undefined &&
    blueprint.player.interactiveProgressbar !== undefined &&
    blueprint.player.volumeControl !== undefined &&
    blueprint.player.defaultVolume !== undefined &&
    blueprint.player.minVolume !== undefined &&
    blueprint.player.muteControl !== undefined &&
    blueprint.player.interactiveMuteControl !== undefined &&
    blueprint.player.hintLabel !== undefined &&
    blueprint.player.hintLabelDelay !== undefined &&
    blueprint.player.activeAfterID !== undefined &&
    blueprint.player.minRuns !== undefined &&
    blueprint.player.maxRuns !== undefined &&
    blueprint.player.showRestRuns !== undefined &&
    blueprint.player.showRestTime !== undefined &&
    blueprint.player.playbackTime !== undefined;
}

export abstract class PlayerElement extends UIElement implements PlayerElementBlueprint {
  player: PlayerProperties;

  protected constructor(element?: PlayerElementBlueprint) {
    super(element);
    if (element && isValidPlayerElementBlueprint(element)) {
      this.player = element.player;
    } else {
      if (environment.strictInstantiation) {
        throw new InstantiationEror('Error at PlayerElement instantiation', element);
      }
      this.player = PropertyGroupGenerators.generatePlayerProps(element?.player);
    }
  }

  getVariableInfos(): VariableInfo[] {
    return [{
      id: this.id,
      type: 'number',
      format: '',
      multiple: false,
      nullable: false,
      values: [],
      valuePositionLabels: [],
      page: '',
      valuesComplete: false
    }];
  }
}

export interface PositionedUIElement extends UIElement {
  position: PositionProperties;
  dimensions: DimensionProperties;
}

export interface PlayerElement extends UIElement {
  player: PlayerProperties;
}

export type TooltipPosition = 'left' | 'right' | 'above' | 'below';

export interface GeometryValue {
  appDefinition: string;
  variables: GeometryVariable[];
}

export interface GeometryVariable {
  id: string;
  value: string;
}