import { Component, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ElementRef, ViewEncapsulation, inject } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { tap, take, map, catchError, switchMap, filter, shareReplay, pairwise, retry } from 'rxjs/operators';
import { of, BehaviorSubject, Subject, combineLatest, Observable } from 'rxjs';
import { State } from '@progress/kendo-data-query';
import { PriceIndexService, PriceIndexDetailItem, IndexAlias, HybridIndexFunction, IndexSuffix, HybridVariable } from './price-index.service';
import { MessageService } from '../_shared/services/message.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DialogService, DialogSettings } from '@progress/kendo-angular-dialog';
import { NotifyService } from '../_shared/services/notify.service';
import * as util from '../_shared/utils/util';
import { CellClickEvent } from '@progress/kendo-angular-grid';
import { FAST_KENDO_COMMON, FAST_PAGE_COMMON } from '../app.config';
import { FastTextAreaComponent } from '../_shared/elements/fast-textarea.component';

@Component({
  selector: 'app-price-index',
  templateUrl: './price-index.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  imports: [FAST_PAGE_COMMON, FAST_KENDO_COMMON]
})
export class PriceIndexComponent {
  @ViewChild('hybridFormulaElem', { static: false }) hybridFormulaElem: ElementRef;

  util = util;
  icons = util.icons;
  exporting: boolean = false;
  loading: boolean = true;
  hasModifyPermission = false;
  mySelection: number[] = [];

  formEditor: FormGroup;
  editorOpened: boolean = false;
  editorLoading$ = new BehaviorSubject<boolean>(true); //this needs to be observable for filterIdNames

  aliasFormEditor: FormGroup;
  aliasEditorOpened: boolean = false;

  publicationEditorOpened: boolean = false;

  hybridFormulaCaretPosition: number = 0;
  topHybridFunctionItem: HybridIndexFunction = { definition: 'Functions', textFiller: '' };
  topIndexSuffixItem: IndexSuffix = { definition: 'Suffixes', textFiller: '' };
  topHybridVariableItem: HybridVariable = { definition: 'Variables', textFiller: '' };

  state: State = {
    filter: null,
    group: null,
    skip: 0,
    sort: [{ field: 'name', dir: 'asc' }],
    take: 100
  };

  titleService = inject(Title);
  messageService = inject(MessageService);
  priceIndexService = inject(PriceIndexService);
  fb = inject(FormBuilder);
  ref = inject(ChangeDetectorRef);
  dialogService = inject(DialogService);
  notify = inject(NotifyService);
  constructor() {
    this.formEditor = this.fb.group({
      id: [null, Validators.required],
      name: [null, Validators.required],
      aliases: [[]],
      indexType: [null, Validators.required],
      commodityId: [null, Validators.required],
      classificationTypeId: [null, Validators.required],
      publicationId: [null],
      description: [null],
      notes: [null],
      hybridFormula: [null, Validators.required],
      unitPriceId: [null, Validators.required],
      forwardCurveId: [null],
      formulaIndex: [null],
      formulaFunction: [''],
      indexSuffix: [''],
      hybridVariable: ['']
    });

    this.aliasFormEditor = this.fb.group({
      id: [null, Validators.required],
      aliasName: [null, Validators.required],
      aliasTypeId: [null, Validators.required]
    })

    this.formEditor.disable();

    //disable these controls when they are not shown so that their validations are not triggered
    this.formEditor.get('indexType').valueChanges.subscribe(indexType => {
      if (this.formEditor.enabled) {
        if (indexType === 2) {  //if hybrid
          setTimeout(() => {
            this.formEditor.get('unitPriceId').disable()
            this.formEditor.get('forwardCurveId').disable()
            this.formEditor.get('hybridFormula').enable()
          })
        }
        else {
          setTimeout(() => {
            this.formEditor.get('unitPriceId').enable()
            this.formEditor.get('forwardCurveId').enable()
            this.formEditor.get('hybridFormula').disable()
          })
        }
      }
    })

    const aliases$: Observable<[IndexAlias[], util.IdName[]]> = combineLatest([this.formEditor.get('aliases').valueChanges, this.aliasTypes$]);
    aliases$.subscribe(
      ([aliases, aliasTypes]) => {
        if (aliases && aliases.length > 0) {
          aliases.forEach(alias => {
            const aliasTypeName = aliasTypes.find(x => x.id === alias.aliasTypeId).name;
            alias.label = aliasTypeName + ' {' + alias.aliasName + '}';
          })
        }
      })

    const formulaIndex$: Observable<util.IdName> = this.formEditor.get('formulaIndex').valueChanges;
    formulaIndex$.pipe(pairwise()).subscribe(([prevFormulaIndex, nextFormulaIndex]) => {
      //if formulaIndex was set to a non-null value and it is different than the previous value
      if (nextFormulaIndex && (prevFormulaIndex?.id !== nextFormulaIndex.id))
        this.insertIntoHybridFormula('[' + nextFormulaIndex.name + ']');
    })

    const formulaFunction$: Observable<HybridIndexFunction> = this.formEditor.get('formulaFunction').valueChanges;
    formulaFunction$.pipe(pairwise()).subscribe(([prevFormulaFunction, nextFormulaFunction]) => {
      //if formulaFunction was set to a non-empty value and it is different than the previous value
      if (!util.isNullOrWhitespace(nextFormulaFunction?.textFiller) && (prevFormulaFunction?.definition !== nextFormulaFunction.definition))
        this.insertIntoHybridFormula(nextFormulaFunction.textFiller);
    })

    const indexSuffix$: Observable<IndexSuffix> = this.formEditor.get('indexSuffix').valueChanges;
    indexSuffix$.pipe(pairwise()).subscribe(([prevIndexSuffix, nextIndexSuffix]) => {
      //if indexSuffix was set to a non-empty value and it is different than the previous value
      if (!util.isNullOrWhitespace(nextIndexSuffix?.textFiller) && (prevIndexSuffix?.definition !== nextIndexSuffix.definition))
        this.appendToHybridFormulaIndex(nextIndexSuffix.textFiller);
    })

    const hybridVariable$: Observable<HybridVariable> = this.formEditor.get('hybridVariable').valueChanges;
    hybridVariable$.pipe(pairwise()).subscribe(([prevHybridVariable, nextHybridVariable]) => {
      //if hybridVariable was set to a non-empty value and it is different than the previous value
      if (!util.isNullOrWhitespace(nextHybridVariable?.textFiller) && (prevHybridVariable?.definition !== nextHybridVariable.definition))
        this.insertIntoHybridFormula(nextHybridVariable.textFiller);
    })
  }

  appendToHybridFormulaIndex(textToInsert: string) {
    const oldHybridFormula: string = this.formEditor.get('hybridFormula').value ?? '';;

    let isInsideIndex = false;
    const restOfFormula: string = oldHybridFormula.substr(this.hybridFormulaCaretPosition, oldHybridFormula.length - this.hybridFormulaCaretPosition);
    const restOfFormulaChars = [...restOfFormula];
    for (const c of restOfFormulaChars) {
      if (c === "[") {
        isInsideIndex = false;
        break;
      }
      else if (c === "]") {
        isInsideIndex = true;
        break;
      }
    }

    const currentPosition = this.hybridFormulaCaretPosition;
    let previousPosition = this.hybridFormulaCaretPosition - 1;
    if (previousPosition < 0)
      previousPosition = 0;

    //if previous character is a closing bracket then append suffix to previous index
    if (oldHybridFormula.substr(previousPosition, 1) === "]")
      this.hybridFormulaCaretPosition -= 1;
    //if next bracket is a an opening bracket then append suffix to next index
    else if (oldHybridFormula.substr(currentPosition, 1) === "[") {
      for (const [index, char] of restOfFormulaChars.entries()) {
        if (index === 0)
          this.hybridFormulaCaretPosition += 1;
        else if (char === "[" || char === "]")
          break;
        else
          this.hybridFormulaCaretPosition += 1;
      }
    }
    else if (isInsideIndex) {
      for (const [, char] of restOfFormulaChars.entries()) {
        if (char === "[" || char === "]")
          break;
        else
          this.hybridFormulaCaretPosition += 1;
      }
    }

    const newHybridFormula = oldHybridFormula.insert(this.hybridFormulaCaretPosition, textToInsert);
    this.formEditor.patchValue({ hybridFormula: newHybridFormula, formulaIndex: null, formulaFunction: this.topHybridFunctionItem, indexSuffix: this.topIndexSuffixItem, hybridVariable: this.topHybridVariableItem });
    this.filterIndexIdNames$.next(null); //this resets the filter for indexIdNames$
    this.hybridFormulaCaretPosition += textToInsert.length + 1;
    util.setCaretPosition("hybridFormula", this.hybridFormulaCaretPosition);
    this.hybridFormulaElem.nativeElement.focus();
  }

  insertIntoHybridFormula(textToInsert: string) {
    const oldHybridFormula: string = this.formEditor.get('hybridFormula').value ?? '';;
    const newHybridFormula = oldHybridFormula.insert(this.hybridFormulaCaretPosition, textToInsert);
    this.formEditor.patchValue({ hybridFormula: newHybridFormula, formulaIndex: null, formulaFunction: this.topHybridFunctionItem, indexSuffix: this.topIndexSuffixItem, hybridVariable: this.topHybridVariableItem });
    this.filterIndexIdNames$.next(null); //this resets the filter for indexIdNames$
    this.hybridFormulaCaretPosition += textToInsert.length;
    util.setCaretPosition("hybridFormula", this.hybridFormulaCaretPosition);
    this.hybridFormulaElem.nativeElement.focus();
  }

  exportClicked$ = new Subject()

  refreshIndexes$ = new BehaviorSubject<string>(null)
  refreshIndexDetail$ = new BehaviorSubject<number>(null)
  refreshRequiredData$ = new BehaviorSubject(null)

  filterCommodities$ = new BehaviorSubject<string>(null)
  filterClassifications$ = new BehaviorSubject<string>(null)
  filterPublications$ = new BehaviorSubject<string>(null)
  filterPriceUnits$ = new BehaviorSubject<string>(null)
  filterIndexIdNames$ = new BehaviorSubject<string>(null)

  saveIndex$ = new Subject<util.SaveType>()
  deleteIndex$ = new Subject()
  testHybridFormula$ = new Subject<string>()

  title$ = of('Price Index').pipe(
    tap((title) => util.trySetTitle(this.titleService, title))
  )

  requiredData$ = this.refreshRequiredData$.pipe(
    tap(() => this.editorLoading$.next(true)),
    switchMap(refreshType => {
      return combineLatest([this.priceIndexService.requiredData$, of(refreshType)]);
    }),
    map(([requiredData, refreshType]) => {
      if (refreshType === util.RefreshType.SelfOnly)
        this.editorLoading$.next(false);
      return requiredData;
    }),
    tap(requiredData => {
      this.hasModifyPermission = requiredData.hasModifyPermission;
      util.focusInputTarget()
    }),
    shareReplay(1),
    catchError(err => {
      return util.handleError(err, this.messageService)
    }), retry(10)
  )

  commodities$ = this.filterCommodities$.pipe(util.filterIdNames(this.editorLoading$, this.requiredData$, 'commodities'));
  classifications$ = this.filterClassifications$.pipe(util.filterIdNames(this.editorLoading$, this.requiredData$, 'classifications'));
  publications$ = this.filterPublications$.pipe(util.filterIdNames(this.editorLoading$, this.requiredData$, 'publications'));
  indexTypes$ = this.requiredData$.pipe(map(x => x.indexTypes));
  priceUnits$ = this.filterPriceUnits$.pipe(util.filterIdNames(this.editorLoading$, this.requiredData$, 'priceUnits'));
  indexIdNames$ = this.filterIndexIdNames$.pipe(util.filterIdNames(this.editorLoading$, this.requiredData$, 'indexIdNames'));
  aliasTypes$ = this.requiredData$.pipe(map(x => x.aliasTypes));
  functions$ = this.requiredData$.pipe(map(x => x.functions));
  suffixes$ = this.requiredData$.pipe(map(x => x.suffixes));
  variables$ = this.requiredData$.pipe(map(x => x.variables));

  priceIndexes$ = this.refreshIndexes$.pipe(
    tap(() => {
      this.loading = true
    }),
    switchMap(() => {
      return this.priceIndexService.getPriceIndexes(this.state);
    }),
    tap(() => {
      this.loading = false;
    }),
    catchError(err => {
      return util.handleError(err, this.messageService)
    })
  )

  exportAction$ = this.exportClicked$.pipe(
    switchMap(() => {
      this.exporting = true
      return this.priceIndexService.exportPriceIndexItems(this.state, 'Price Indexes.xlsx');
    }),
    tap(res => {
      util.openOrSaveFile(res.fileBlob, res.fileName);
      this.exporting = false;
    }),
    shareReplay(1),
    catchError(err => {
      this.exporting = false;
      return util.handleError(err, this.messageService)
    }), retry(10)
  )

  indexDetail$ = this.refreshIndexDetail$.pipe(
    filter(id => id !== null),
    switchMap(id => {
      return this.priceIndexService.getPriceIndexDetail(id);
    }),
    tap((indexDetail) => {
      indexDetail.formulaIndex = null;
      indexDetail.formulaFunction = this.topHybridFunctionItem;
      indexDetail.indexSuffix = this.topIndexSuffixItem;
      indexDetail.hybridVariable = this.topHybridVariableItem;
      this.setEditorItem(indexDetail);
      this.editorFinishedLoading();
      this.hybridFormulaCaretPosition = indexDetail.hybridFormula?.length ?? 0;
    }),
    shareReplay(1),
    catchError(err => {
      return util.handleError(err, this.messageService);
    })
  )

  saveIndexResult$ = this.saveIndex$.pipe(
    switchMap(saveType => {
      this.editorLoading$.next(true);
      this.formEditor.disable();
      const itemToSave: PriceIndexDetailItem = this.formEditor.value;
      return this.priceIndexService.savePriceIndex(itemToSave, saveType);
    }),
    tap((hybridTestResult) => {
      if (hybridTestResult.isValid) {
        this.notify.success('save successful');
        this.editorFinishedLoading();
        this.editorOpened = false;
        this.refreshIndexes$.next(null);
        this.refreshRequiredData$.next(null);
        this.mySelection = [hybridTestResult.hybridIndexId];
      }
      else { //invalid hybrid formula
        this.editorFinishedLoading();
        this.messageService.throw(hybridTestResult.message);
      }
    }),
    shareReplay(1),
    catchError(err => {
      this.editorFinishedLoading();
      return util.handleError(err, this.messageService)
    }), retry(10)
  )

  deleteIndexResult$ = this.deleteIndex$.pipe(
    switchMap(() => {
      const itemToDelete: PriceIndexDetailItem = this.formEditor.value;
      return this.priceIndexService.deletePriceIndex(itemToDelete.id);
    }),
    tap(() => {
      this.notify.success('delete successful');
      this.editorFinishedLoading();
      this.editorOpened = false;
      this.refreshIndexes$.next(null);
    }),
    catchError(err => {
      return util.handleError(err, this.messageService)
    })
  )

  testHybridFormulaResult$ = this.testHybridFormula$.pipe(
    switchMap((formula) => {
      return this.priceIndexService.testHybridFormula(formula);
    }),
    tap((result) => {
      if (result.isValid)
        this.messageService.info(result.message);
      else
        this.messageService.throw(result.message);
    }),
    catchError(err => {
      return util.handleError(err, this.messageService)
    })
  )

  setEditorItem(editorItem: PriceIndexDetailItem): void {
    this.formEditor.setValue(editorItem);
  }

  openEditor(id: number): void {
    this.formEditor.disable();
    this.editorLoading$.next(true);
    this.formEditor.reset();

    //set new values for an 'add' action
    this.hybridFormulaCaretPosition = 0;
    this.formEditor.patchValue({ id: id, indexType: 0, aliases: [], commodityId: 1, unitPriceId: 4, classificationTypeId: 0, notes: '', description: '', hybridFormula: '', formulaFunction: this.topHybridFunctionItem, indexSuffix: this.topIndexSuffixItem, hybridVariable: this.topHybridFunctionItem });
    this.editorOpened = true;
  }

  openAliasAdder() {
    this.aliasFormEditor.reset();
    this.aliasFormEditor.patchValue({ id: 0, aliasTypeId: 1 });
    this.aliasEditorOpened = true;
    util.focusInputTarget();
  }

  openPublicationEditor() {
    this.publicationEditorOpened = true;
  }

  closePublicationEditor() {
    this.refreshRequiredData$.next(util.RefreshType.SelfOnly);
    this.publicationEditorOpened = false;
  }

  editorFinishedLoading(): void {
    this.editorLoading$.next(false);
    this.formEditor.enable();
  }

  addIndex(): void {
    this.openEditor(0);
    this.editorFinishedLoading();
    util.focusInputTarget();
  }

  editIndex(args: CellClickEvent): void {
    this.openEditor(args.dataItem.id);
    this.refreshIndexDetail$.next(args.dataItem.id);
  }

  saveIndex(instance: PriceIndexComponent, saveType: util.SaveType): void {
    this.formEditor.markAllAsTouched();
    if (this.formEditor.valid) {
      this.saveIndex$.next(saveType);
    } else {
      this.notify.error("validation failed");
    }
  }

  deleteIndex(): void {
    const deleteConfirmSettings: DialogSettings = {
      title: "Please confirm",
      content: "Are you sure you want to delete this index?",
      actions: [{ text: 'No' }, { text: 'Yes', cssClass: 'k-primary' }],
      cssClass: 'utilPrompt'
    }

    this.dialogService.open(deleteConfirmSettings).result.pipe(take(1)).subscribe(result => {
      if (util.getDialogAction(result) === util.dialogAction.Yes) {
        this.formEditor.disable();
        this.editorLoading$.next(true);
        this.deleteIndex$.next(null);
      }
    });
  }

  export(): void {
    this.exportClicked$.next(null);
  }

  dataStateChange(state: State): void {
    this.state = state;
    this.refreshIndexes$.next(null);
  }

  filterCommodities(value: string) {
    this.filterCommodities$.next(value);
  }

  filterClassifications(value: string) {
    this.filterClassifications$.next(value);
  }

  filterPublications(value: string) {
    this.filterPublications$.next(value);
  }

  filterPriceUnits(value: string) {
    this.filterPriceUnits$.next(value);
  }

  filterIndexIdNames(value: string) {
    this.filterIndexIdNames$.next(value);
  }

  onAliasesOpen(event: Event) {
    event.preventDefault();
  }

  addAlias() {
    this.aliasFormEditor.markAllAsTouched();
    if (this.aliasFormEditor.valid) {
      let aliases: IndexAlias[] = this.formEditor.get('aliases').value;
      const newAlias: IndexAlias = this.aliasFormEditor.value;
      if (aliases && aliases.length > 0) {
        const maxId = aliases.sort((a1, a2) => a2.id - a1.id)[0].id;
        newAlias.id = maxId + 1;
      }
      if (aliases === null)
        aliases = [];
      aliases.push(newAlias);
      this.formEditor.patchValue({ aliases: aliases });
      this.aliasEditorOpened = false;
    } else {
      this.notify.error("validation failed");
    }
  }

  testHybridFormula() {
    const formula: string = this.formEditor.get('hybridFormula').value;
    this.testHybridFormula$.next(formula);
  }

  setHybridFormulaCaretPosition(fastTextArea: FastTextAreaComponent) {
    const htmlTextArea = fastTextArea.textAreaElement.nativeElement as HTMLTextAreaElement;
    this.hybridFormulaCaretPosition = htmlTextArea.selectionStart ? htmlTextArea.selectionStart : 0;
  }

  refreshDropdowns() {
    this.refreshRequiredData$.next(util.RefreshType.SelfOnly);
  }
}
