/*
 * Copyright (C) 2023 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import moment from 'moment';
import {Subscription} from 'rxjs';
import {logger} from '../_helpers/ts-logger';

@Injectable()
@Pipe({
  name: 'join',
  pure: true,
})
export class ArrayJoinPipe implements PipeTransform {
  value = '';

  transform(anArray: unknown[] | unknown, separator: string | unknown): string {
    if (typeof separator !== 'string' || separator.length <= 0) {
      return this.value;
    }

    if (!Array.isArray(anArray)) {
      throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
    }

    this.value = anArray.join(separator);

    return this.value;
  }
}

@Injectable()
@Pipe({
  name: 'entries',
  pure: true,
})
export class EntriesPipe implements PipeTransform {
  transform<T>(value: Record<string | number | symbol, T>): T[] {
    return Object.values(value);
  }
}

@Injectable()
@Pipe({
  name: 'toUnix',
  pure: true,
})
export class ToUnixPipe implements PipeTransform {
  transform(value: string | number | Date | null | undefined): number {
    return (value instanceof Date ? value : new Date(value ?? 0)).valueOf();
  }
}

@Injectable()
@Pipe({
  name: 'sentencecase',
  pure: true,
})
export class SentenceCasePipe implements PipeTransform {
  value = '';

  transform(aString: string | unknown): string {
    if (typeof aString !== 'string') {
      throw new SyntaxError(
        `Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
      );
    }

    this.value = aString.slice(0, 1).toUpperCase() + aString.slice(1);

    return this.value;
  }
}

@Injectable()
@Pipe({
  name: 'split',
  pure: true,
})
export class StringSplitPipe implements PipeTransform {
  value = new Array<unknown>();

  transform(aString: string | unknown, splitter: string | unknown): unknown[] {
    if (typeof splitter !== 'string' || splitter.length <= 0) {
      return this.value as never;
    }

    if (typeof aString !== 'string') {
      throw new SyntaxError(
        `Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
      );
    }

    this.value = aString.split(splitter);

    return this.value as never;
  }
}
@Injectable()
@Pipe({
  name: 'durationLocalized',
  pure: true,
})
export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
  locale: string;

  onLangChange?: Subscription;

  value: string;

  frequencyPrefixes: {[iso6391Code: string]: string} = {
    de: 'alle',
    en: 'every',
    es: 'cada',
    pt: 'a cada',
    fr: 'tous les',
    cn: '每',
    ru: 'kаждые',
  };

  constructor(private readonly translate: TranslateService) {
    this.locale = translate.currentLang;
  }

  private _dispose(): void {
    if (this.onLangChange?.closed === false) {
      this.onLangChange?.unsubscribe();
    }
  }

  ngOnDestroy(): void {
    this._dispose();
  }

  /**
   * @param value An ISO 8601 duration string
   * @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency
   */
  transform(value: string | unknown, isFrequency = false): string {
    this.updateValue(value, isFrequency);
    this._dispose();
    if (this.onLangChange?.closed === true) {
      this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
        this.locale = event.lang;
        this.updateValue(value, isFrequency);
      });
    }

    return this.value;
  }

  updateValue(value: string | unknown, isFrequency = false): void {
    if (typeof value !== 'string') {
      logger.warn(`durationLocalized pipe unable to parse input: ${value}`);

      return;
    }

    if (isFrequency) {
      const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
        this.locale.includes(element),
      );
      this.value = [
        fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
        moment.duration(value).humanize(),
      ].join(' ');
    } else {
      this.value = moment.duration(value).humanize();
    }
  }
}

@Injectable()
@Pipe({
  name: 'metersLocalized',
  pure: false,
})
export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
  locale: string;

  onLangChange?: Subscription;

  value = '';

  constructor(private readonly translate: TranslateService) {
    this.locale = translate.currentLang;
  }

  private _dispose(): void {
    if (this.onLangChange?.closed === false) {
      this.onLangChange?.unsubscribe();
    }
  }

  ngOnDestroy(): void {
    this._dispose();
  }

  transform(value: string | number | unknown): string {
    this.updateValue(value);
    this._dispose();
    if (this.onLangChange?.closed === true) {
      this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
        this.locale = event.lang;
        this.updateValue(value);
      });
    }

    return this.value;
  }

  updateValue(value: string | number | unknown) {
    if (typeof value !== 'string' && typeof value !== 'number') {
      logger.warn(`metersLocalized pipe unable to parse input: ${value}`);

      return;
    }
    const imperialLocale = ['US', 'UK', 'LR', 'MM'].some(term => this.locale.includes(term));
    const meters = typeof value === 'string' ? Number.parseFloat(value) : (value as number);

    if (imperialLocale) {
      const yards = meters * 1.0936;
      const options = {
        style: 'unit',
        unit: yards >= 1760 ? 'mile' : 'yard',
        maximumFractionDigits: yards >= 1760 ? 1 : 0,
      } as unknown as Intl.NumberFormatOptions;
      this.value = new Intl.NumberFormat(this.locale, options).format(yards >= 1760 ? yards / 1760 : yards);
    } else {
      const options = {
        style: 'unit',
        unit: meters >= 1000 ? 'kilometer' : 'meter',
        maximumFractionDigits: meters >= 1000 ? 1 : 0,
      } as unknown as Intl.NumberFormatOptions;
      this.value = new Intl.NumberFormat(this.locale, options).format(
        meters >= 1000 ? meters / 1000 : meters,
      );
    }
  }
}

@Injectable()
@Pipe({
  name: 'isNaN',
  pure: true,
})
export class IsNaNPipe implements PipeTransform {
  transform(value: unknown): boolean {
    return Number.isNaN(value);
  }
}

@Injectable()
@Pipe({
  name: 'isNumeric',
  pure: true,
})
export class IsNumericPipe implements PipeTransform {
  transform(value: unknown): boolean {
    return !Number.isNaN(
      typeof value === 'number' ? value : typeof value === 'string' ? Number.parseFloat(value) : Number.NaN,
    );
  }
}

@Injectable()
@Pipe({
  name: 'numberLocalized',
  pure: true,
})
export class NumberLocalizedPipe implements PipeTransform, OnDestroy {
  locale: string;

  onLangChange?: Subscription;

  value: string;

  constructor(private readonly translate: TranslateService) {
    this.locale = translate.currentLang;
  }

  private _dispose(): void {
    if (this.onLangChange?.closed === false) {
      this.onLangChange?.unsubscribe();
    }
  }

  ngOnDestroy(): void {
    this._dispose();
  }

  /**
   * @param value The number to be formatted
   * @param formatOptions Formatting options to include.
   * As specified by Intl.NumberFormatOptions as comma seperated key:value pairs.
   */
  transform(value: string | number | unknown, formatOptions?: string): string {
    this.updateValue(value, formatOptions);
    this._dispose();
    if (this.onLangChange?.closed === true) {
      this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
        this.locale = event.lang;
        this.updateValue(value, formatOptions);
      });
    }

    return this.value;
  }

  updateValue(value: string | number | unknown, formatOptions?: string): void {
    if (typeof value !== 'string' && typeof value !== 'number') {
      logger.warn(`numberLocalized pipe unable to parse input: ${value}`);

      return;
    }
    const options = formatOptions
      ?.split(',')
      .map(element => element.split(':'))
      .reduce(
        (accumulator, [key, value_]) => ({
          ...accumulator,
          [key.trim()]: value_.trim(),
        }),
        {},
      ) as Intl.NumberFormatOptions;
    const float = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
    this.value = new Intl.NumberFormat(this.locale, options).format(float);
  }
}

@Injectable()
@Pipe({
  name: 'dateFormat',
  pure: true,
})
export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy {
  locale: string;

  onLangChange?: Subscription;

  value: string;

  constructor(private readonly translate: TranslateService) {
    this.locale = translate.currentLang;
  }

  private _dispose(): void {
    if (this.onLangChange?.closed === false) {
      this.onLangChange?.unsubscribe();
    }
  }

  ngOnDestroy(): void {
    this._dispose();
  }

  /**
   * @param value The date to be formatted
   * @param formatOptions Dateformat options to include.
   * As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs
   * Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35"
   */
  transform(value: string | unknown, formatOptions?: string): string {
    this.updateValue(value, formatOptions);
    this._dispose();
    if (this.onLangChange?.closed === true) {
      this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
        this.locale = event.lang;
        this.updateValue(value, formatOptions);
      });
    }

    return this.value;
  }

  updateValue(value: string | Date | unknown, formatOptions?: string): void {
    if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
      logger.warn(`dateFormat pipe unable to parse input: ${value}`);

      return;
    }
    const options = formatOptions
      ?.split(',')
      .map(element => element.split(':'))
      .reduce(
        (accumulator, [key, value_]) => ({
          ...accumulator,
          [key.trim()]: value_.trim(),
        }),
        {},
      ) as Intl.DateTimeFormatOptions;
    const date = typeof value === 'string' ? Date.parse(value) : (value as Date);
    this.value = new Intl.DateTimeFormat(
      this.locale,
      options ?? {
        day: 'numeric',
        month: 'numeric',
        year: 'numeric',
        hour: 'numeric',
        minute: 'numeric',
      },
    ).format(date);
  }
}
