import naturalSort from 'javascript-natural-sort';
import lunr, {PipelineFunction} from 'lunr';
import lunrDe from 'lunr-languages/lunr.de';
import lunrFr from 'lunr-languages/lunr.fr';
import lunrIt from 'lunr-languages/lunr.it';
import lunrRu from 'lunr-languages/lunr.ru';
import lunrStemmer from 'lunr-languages/lunr.stemmer.support';
import {JsonFam, JsonGedcomData, JsonIndi} from 'topola';
import {idToFamMap, idToIndiMap} from '../util/gedcom_util';

lunrStemmer(lunr);
lunrDe(lunr);
lunrFr(lunr);
lunrIt(lunr);
lunrRu(lunr);

const MAX_RESULTS = 8;

export interface SearchResult {
  id: string;
  indi: JsonIndi;
}

export interface SearchIndex {
  search(input: string): SearchResult[];
}

/** Removes accents from letters, e.g. ó->o, ę->e. */
function normalize(input: string) {
  return input
    .toLocaleLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/\u0142/g, 'l'); // Special case: ł is not affected by NFD.
}

/** Comparator to sort by score first, then by id. */
function compare(a: lunr.Index.Result, b: lunr.Index.Result) {
  if (a.score !== b.score) {
    return b.score - a.score;
  }
  return naturalSort(a.ref, b.ref);
}

/** Returns all last names of all husbands as a space-separated string. */
function getHusbandLastName(
  indi: JsonIndi,
  indiMap: Map<string, JsonIndi>,
  famMap: Map<string, JsonFam>,
): string {
  return (indi.fams || [])
    .map((famId) => famMap.get(famId))
    .map((fam) => fam && fam.husb)
    .map((husbId) => husbId && indiMap.get(husbId))
    .map((husband) => husband && husband.lastName)
    .join(' ');
}

class LunrSearchIndex implements SearchIndex {
  private index!: lunr.Index;
  private indiMap: Map<string, JsonIndi>;
  private famMap: Map<string, JsonFam>;

  constructor(data: JsonGedcomData) {
    this.indiMap = idToIndiMap(data);
    this.famMap = idToFamMap(data);
  }

  initialize() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const self = this;
    this.index = lunr(function () {
      //Trimmer will break non-latin characters, so custom multilingual implementation must be used
      self.initMultiLingualLunrWithoutTrimmer(this, [
        'de',
        'en',
        'fr',
        'it',
        'ru',
      ]);
      this.ref('id');
      this.field('id');
      this.field('name', {boost: 10});
      this.field('normalizedName', {boost: 8});
      this.field('spouseLastName', {boost: 2});
      this.field('normalizedSpouseLastName', {boost: 2});

      self.indiMap.forEach((indi) => {
        const name = [indi.firstName, indi.lastName].join(' ');
        const spouseLastName = getHusbandLastName(
          indi,
          self.indiMap,
          self.famMap,
        );
        this.add({
          id: indi.id,
          name,
          normalizedName: normalize(name),
          spouseLastName,
          normalizedSpouseLastName: normalize(spouseLastName),
        });
      });
    });
  }

  private initMultiLingualLunrWithoutTrimmer(
    lunrInstance: lunr.Builder,
    languages: string[],
  ): void {
    const pipelineFunctions: PipelineFunction[] = [];
    const searchPipelineFunctions: PipelineFunction[] = [];
    languages.forEach((language) => {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      const lunrLanguage = lunr[language];
      if (language === 'en') {
        pipelineFunctions.unshift(lunr.stopWordFilter);
        pipelineFunctions.push(lunr.stemmer);
        searchPipelineFunctions.push(lunr.stemmer);
      } else {
        if (lunrLanguage.stopWordFilter) {
          pipelineFunctions.unshift(lunrLanguage.stopWordFilter);
        }
        if (lunrLanguage.stemmer) {
          pipelineFunctions.push(lunrLanguage.stemmer);
          searchPipelineFunctions.push(lunrLanguage.stemmer);
        }
      }
    });
    lunrInstance.pipeline.reset();
    lunrInstance.pipeline.add(...pipelineFunctions);

    if (lunrInstance.searchPipeline) {
      lunrInstance.searchPipeline.reset();
      lunrInstance.searchPipeline.add(...searchPipelineFunctions);
    }
  }

  public search(input: string): SearchResult[] {
    const query = input
      .split(' ')
      .filter((s) => !!s)
      .map((s) => `${s} ${s}*`)
      .join(' ');
    const results = this.index.search(query);
    return results
      .sort(compare)
      .slice(0, MAX_RESULTS)
      .map((result) => ({
        id: result.ref,
        indi: this.indiMap.get(result.ref) || {
          id: result.ref,
          firstName: 'INDI NOT FOUND',
        },
      }));
  }
}

/** Builds a search index from data. */
export function buildSearchIndex(data: JsonGedcomData): SearchIndex {
  const index = new LunrSearchIndex(data);
  index.initialize();
  return index;
}
