/*
 * Copyright (C) 2020 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { BOSCH_DDS_COLORS } from '@bci-web-core/core';
import { GaugeCircle } from './circle.model';

type GaugeState = 'ok' | 'warn' | 'error' | 'unknown';

/**
 * Reusable component that displays a value in a half-circle gauge. It features:
 * - Current value & unit
 * - minimum and maximum value
 * - A value name/label
 * - and more (see @Inputs)
 */
@Component({
  selector: 'bci-circular-gauge',
  templateUrl: './circular-gauge.component.html',
  styleUrls: ['./circular-gauge.component.scss'],
})
export class CircularGaugeComponent implements OnInit, OnChanges {
  private readonly MIN_ANGLE = 0;
  private readonly MAX_ANGLE = 180;
  private readonly ARC_ROTATION_OFFSET = 180;
  private readonly GAUGE_CIRCLE = new GaugeCircle({ x: 200, y: 200 }, 160, this.ARC_ROTATION_OFFSET);
  private readonly INDICATOR_WIDTH = 1.4;

  /** An optional unit that is displayed next to the value */
  @Input() unit: string;
  /** The mandatory minimum value that represent the left most value that can be displayed on the chart */
  @Input() minValue = 0;
  /** The mandatory maximum value that represent the right most value that can be displayed on the chart */
  @Input() maxValue = 100;
  /** The an optional warning threshold. When value is below that threshold, the gauge is colored in yellow */
  @Input() warnThreshold: number;
  /** The an optional error threshold. When value is below that threshold, the gauge is colored in red */
  @Input() errorThreshold: number;
  /** The an optional target value that represents the value that is considered as the desired value in the given context */
  @Input() targetValue: number;
  /** The an optional string that describes the value and is displayed below the gauge */
  @Input() valueName: string;
  /** If set to false, the min and max values are not displayed on the chart. */
  @Input() displayRange = true;
  /** Number of decimal points to be displayed for the current value.  */
  @Input() digits = 1;
  /** If set to true, the threshold logic will be inverted. E.g. when the value is above warn, the gauge will be yellow. */
  @Input() invertThresholds = false;

  gaugeArcPath: string;
  warnThresholdArcPath: string;
  errorThresholdArcPath: string;
  targetValueArcPath: string;
  boschColors = BOSCH_DDS_COLORS;
  gaugeState: GaugeState = 'unknown';
  warnThresholdIndicatorInverted: boolean;
  errorThresholdIndicatorInverted: boolean;

  get currentValue(): number {
    return this._currentValue;
  }

  @Input()
  set currentValue(value: number) {
    this._currentValue = value;
    this.updateGauge();
  }

  private _currentValue: number;

  ngOnInit(): void {
    this.updateGauge();
  }

  ngOnChanges(changes: SimpleChanges): void {
    if (this.inputsChanged(changes)) {
      this.updateGauge();
    }
  }

  valueToPercentage(x: number): number {
    const valueRange = Math.abs(this.maxValue - this.minValue);
    const valueInRage = x - this.minValue;
    const percentage = (valueInRage / valueRange) * 100;
    return Math.max(Math.min(percentage, 100), 0);
  }

  valueToAngle(x: number): number {
    // eslint-disable-next-line no-extra-boolean-cast
    const percentage = !!x ? this.valueToPercentage(x) : 0;
    return this.MIN_ANGLE + (percentage * (this.MAX_ANGLE - this.MIN_ANGLE)) / 100;
  }

  private updateGauge() {
    const valueAngle = this.valueToAngle(this._currentValue);
    const warnThresholdAngle = this.valueToAngle(this.warnThreshold);
    const errorThresholdAngle = this.valueToAngle(this.errorThreshold);
    const targetValueAngle = this.valueToAngle(this.targetValue);

    this.gaugeArcPath = this.GAUGE_CIRCLE.toSvgArc(this.MIN_ANGLE, valueAngle);
    this.warnThresholdArcPath = this.describeIndicatorArcSegment(warnThresholdAngle);
    this.errorThresholdArcPath = this.describeIndicatorArcSegment(errorThresholdAngle);
    this.targetValueArcPath = this.describeIndicatorArcSegment(targetValueAngle);

    this.gaugeState = this.calculateGaugeState();
    this.updateThresholdIndicators(valueAngle, warnThresholdAngle, errorThresholdAngle);
  }

  private describeIndicatorArcSegment(warnThresholdAngle: number): string {
    return this.GAUGE_CIRCLE.toSvgArc(
      warnThresholdAngle - this.INDICATOR_WIDTH / 2,
      warnThresholdAngle + this.INDICATOR_WIDTH / 2
    );
  }

  private calculateGaugeState(): GaugeState {
    let gaugeState: GaugeState = 'unknown';
    if (this.isErrorThresholdExceeded()) {
      gaugeState = 'error';
    } else if (this.isWarnThresholdExceeded()) {
      gaugeState = 'warn';
    } else if (this.isOk()) {
      gaugeState = 'ok';
    }
    return gaugeState;
  }

  private isOk() {
    return this.invertThresholds
      ? (this.warnThreshold && this._currentValue <= this.warnThreshold) ||
          (this.errorThreshold && this._currentValue <= this.errorThreshold)
      : (this.warnThreshold && this._currentValue >= this.warnThreshold) ||
          (this.errorThreshold && this._currentValue >= this.errorThreshold);
  }

  private isWarnThresholdExceeded(): boolean {
    return this.invertThresholds ? this._currentValue > this.warnThreshold : this._currentValue < this.warnThreshold;
  }

  private isErrorThresholdExceeded(): boolean {
    return this.invertThresholds ? this._currentValue > this.errorThreshold : this._currentValue < this.errorThreshold;
  }

  private updateThresholdIndicators(valueAngle: number, warnThresholdAngle: number, errorThresholdAngle: number): void {
    this.warnThresholdIndicatorInverted = valueAngle > warnThresholdAngle;
    this.errorThresholdIndicatorInverted = valueAngle > errorThresholdAngle;
  }

  private inputsChanged(changes: SimpleChanges): boolean {
    return (
      this.hasInputChanged(changes, 'unit') ||
      this.hasInputChanged(changes, 'minValue') ||
      this.hasInputChanged(changes, 'maxValue') ||
      this.hasInputChanged(changes, 'warnThreshold') ||
      this.hasInputChanged(changes, 'errorThreshold') ||
      this.hasInputChanged(changes, 'targetValue') ||
      this.hasInputChanged(changes, 'valueName') ||
      this.hasInputChanged(changes, 'displayRange') ||
      this.hasInputChanged(changes, 'digits') ||
      this.hasInputChanged(changes, 'invertThresholds')
    );
  }

  private hasInputChanged(changes: SimpleChanges, inputName: string): boolean {
    return changes[inputName] !== undefined && changes[inputName].currentValue !== changes[inputName].previousValue;
  }
}
