/*
 * 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} from '@angular/core';
import {
  SCFacet,
  SCFavorite,
  SCIndexableThings,
  SCSaveableThing,
  SCSearchBooleanFilter,
  SCSearchFilter,
  SCSearchSort,
  SCSearchValueFilter,
  SCThings,
  SCThingType,
  SCUuid,
} from '@openstapps/core';
import {StorageProvider} from '../storage/storage.provider';
import {DataProvider} from '../data/data.provider';
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from '../../translation/thing-translate.service';
import {BehaviorSubject, Observable} from 'rxjs';
import {debounceTime, map} from 'rxjs/operators';

/**
 * Service handling favorites
 */
@Injectable({
  providedIn: 'root',
})
export class FavoritesService {
  /**
   * Translation pipe
   */
  thingTranslatePipe: ThingTranslatePipe;

  favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());

  // using debounce time 0 allows change detection to run through async suspension
  favoritesChanged$ = this.favorites.pipe(debounceTime(0));

  favoriteThings$ = this.favoritesChanged$.pipe(map(favorite => [...favorite.values()].map(it => it.data)));

  static getDataFromFavorites(items: SCFavorite[]) {
    return items.map(item => item.data);
  }

  /**
   * Provides the type value from a filter
   * @param filter Filter to get the type from
   */
  static getFilterType(filter: SCSearchBooleanFilter | SCSearchValueFilter) {
    let value: string | undefined;
    if (filter.type === 'boolean') {
      for (const internalFilter of filter.arguments.filters) {
        value = FavoritesService.getFilterType(internalFilter as SCSearchBooleanFilter | SCSearchValueFilter);
      }
    } else {
      value = filter.arguments.value as string;
    }

    return value;
  }

  /**
   * Provides all the saved favorites
   */
  async getAll() {
    return this.storageProvider.search<SCFavorite>(this.storagePrefix);
  }

  /**
   * Sorts provided items by the provided field
   * @param items Items to sort
   * @param field The field to use for sorting the items
   * @param sortType In which order to sort the provided textual field
   */
  sortItems(items: SCIndexableThings[], field: 'name' | 'type', sortType: 'asc' | 'desc') {
    const reverse = sortType === 'asc' ? 1 : -1;

    return items.sort((a, b) => {
      return (
        new Intl.Collator(this.translate.currentLang).compare(
          this.thingTranslatePipe.transform(field, a),
          this.thingTranslatePipe.transform(field, b),
        ) * reverse
      );
    });
  }

  /**
   * Gets storage prefix text
   */
  get storagePrefix(): string {
    return this._storagePrefix;
  }

  /**
   * Sets storage prefix text
   */
  set storagePrefix(storagePrefix) {
    this._storagePrefix = storagePrefix;
  }

  /**
   * Storage prefix text
   */
  private _storagePrefix = 'stapps.favorites';

  constructor(
    private storageProvider: StorageProvider,
    private readonly translate: TranslateService,
    private readonly thingTranslate: ThingTranslateService,
  ) {
    this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
    void this.emitAll();
  }

  /**
   * Provides key for storing data into the local database
   * @param uid Unique identifier of a resource
   */
  getStorageKey(uid: string): string {
    return `${this.storagePrefix}.${uid}`;
  }

  /**
   * Removes an item from favorites
   * @param item Data item that needs to be deleted
   */
  async delete(item: SCIndexableThings): Promise<void> {
    await this.storageProvider.delete(this.getStorageKey(item.uid));
    void (await this.emitAll());
  }

  /**
   * Save an item as a favorite
   * @param item Data item that needs to be saved
   */
  async put(item: SCIndexableThings): Promise<void> {
    const favorite = DataProvider.createSaveable(item, SCThingType.Favorite);
    await this.storageProvider.put<SCSaveableThing>(this.getStorageKey(item.uid), favorite);
    void (await this.emitAll());
  }

  async emitAll() {
    this.favorites.next(await this.getAll());
  }

  get(uid: SCUuid): Observable<SCFavorite | undefined> {
    return this.favoritesChanged$.pipe(
      map(favoritesMap => {
        return favoritesMap.get(this.getStorageKey(uid));
      }),
    );
  }

  /**
   * Search through the (saved) favorites
   * @param queryText Text to filter the data with
   * @param filterQuery Filters to apply on the data
   * @param sortQuery Sort to apply on the data
   */
  search(
    queryText?: string,
    filterQuery?: SCSearchFilter,
    sortQuery?: SCSearchSort[],
  ): Observable<{data: SCThings[]; facets: SCFacet[]}> {
    return this.favoritesChanged$.pipe(
      map(favoritesMap => {
        let items = [...favoritesMap.values()].map(favorite => favorite.data);
        if (queryText !== undefined) {
          const textFilteredItems: SCIndexableThings[] = [];
          for (const item of items) {
            if (
              this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
            ) {
              textFilteredItems.push(item);
            }
          }
          items = textFilteredItems;
        }

        if (filterQuery !== undefined) {
          const filterType = FavoritesService.getFilterType(
            filterQuery as SCSearchBooleanFilter | SCSearchValueFilter,
          );
          const filteredItems: SCIndexableThings[] = [];
          for (const item of items) {
            if (item.type === filterType) {
              filteredItems.push(item);
            }
          }
          items = filteredItems;
        }

        if (sortQuery !== undefined) {
          items = this.sortItems(items, sortQuery[0].arguments.field as 'name' | 'type', sortQuery[0].order);
        }

        return {
          data: items,
          facets: [DataProvider.facetForField(items, 'type')],
        };
      }),
    );
  }
}
