import { Component, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ViewEncapsulation, HostListener, OnDestroy, ElementRef, ViewChildren, AfterContentInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { tap, map, catchError, switchMap, shareReplay, retry, filter, takeUntil, debounceTime, groupBy, mergeMap, take } from 'rxjs/operators';
import { of, BehaviorSubject, Subject, combineLatest, Observable } from 'rxjs';
import { SosService } from './sos.service';
import { ExternalParams, MeterInfo, PipeInfo, PointInfo, RequiredData, SosLoadOptions, SosPipeContractInfo } from './models';
import { MessageService } from '../_shared/services/message.service';
import { FormArray, 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 * as models from './models';
import dayjs from 'dayjs';
import { DisplayInfo, SosSettingItem, SosSettingsService } from './sos-settings/sos-settings.service';
import { DatePickerComponent } from '@progress/kendo-angular-dateinputs';
import { CommonService } from '../_shared/services/common.service';
import { CustomFormBuilder } from '../_shared/services/custom-form-builder.service';
import { Dictionary } from '../_shared/utils/dictionary';
import { SosTransfersComponent } from './sos-transfers/sos-transfers.component';
import { AdjustType, SosHelper } from './sosHelper';
import { SosSettingsComponent } from './sos-settings/sos-settings.component';
import { ContextMenuSelectEvent, KENDO_CONTEXTMENU } from '@progress/kendo-angular-menu';
import { AddDealParams } from './sos-deal/sos-deal.service';
import { SortDescriptor } from '@progress/kendo-data-query';
import { environment } from '../../environments/environment';
import { SosElemNumericInputComponent } from './controls/sos-elem-numeric-input';
import { SosTransfersService } from './sos-transfers/sos-transfers.service';
import { ActivatedRoute } from '@angular/router';
import { SosNoteComponent } from './sos-note/sos-note.component';
import { FAST_KENDO_COMMON, FAST_PAGE_COMMON } from '../app.config';
import { SosLegendComponent } from './sos-legend/sos-legend.component';
import { HoverClassDirective } from '../_shared/directives/hover-class-directive';
import { SosElemTextComponent } from './controls/sos-elem-text';
import { SosElemComboComponent } from './controls/sos-elem-combo';
import { SosElemTextInputComponent } from './controls/sos-elem-text-input';
import { KFormatNumPipe } from '../_shared/pipes/k-format-num.pipe';
import { SosMarketHeightPipe } from './pipes/sos-market-height.pipe';
import { SosRowMainColorsPipe } from './pipes/sos-row-main-colors.pipe';
import { SosMarketPositionPipe } from './pipes/sos-market-position.pipe';
import { KENDO_DROPDOWNLIST } from '@progress/kendo-angular-dropdowns';
import { SosMarketVolColorPipe } from './pipes/sos-market-vol-color.pipe';
import { FormGroupArrayPipe } from '../_shared/pipes/form-group-array.pipe';
import { SosSaveComponent } from './sos-save/sos-save.component';
import { FastFrameComponent } from '../_shared/elements/fast-frame/fast-frame.component';
import { SosDealComponent } from './sos-deal/sos-deal.component';
import { SosSupplyResetComponent } from './sos-supply-reset/sos-supply-reset.component';
import { NgTemplateOutlet } from '@angular/common';
import { VariableDirective } from '../_shared/directives/variable-directive';
import { SosTransferChangeDatesComponent } from './sos-transfer-change-dates/sos-transfer-change-dates.component';
import { SupplyPipelineContractsPipe } from "./pipes/sos-supply-pipelinecontracts";
import { SosSnapshotHistoryComponent } from './sos-snapshot-history/sos-snapshot-history.component';

export const transferColWidth: number = 110;
export const marketColWidth: number = 140;

@Component({
  selector: 'app-sos',
  templateUrl: './sos.component.html',
  styleUrls: ['./sos.component.scss'],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FAST_PAGE_COMMON, FAST_KENDO_COMMON, NgTemplateOutlet, VariableDirective, SosSnapshotHistoryComponent, SosNoteComponent, SosTransfersComponent, SosLegendComponent, HoverClassDirective, SosElemTextComponent, SosElemComboComponent, SosElemTextInputComponent, SosElemNumericInputComponent, KENDO_CONTEXTMENU, KFormatNumPipe, SosMarketHeightPipe, SosRowMainColorsPipe, SosMarketPositionPipe, KENDO_DROPDOWNLIST, SosMarketVolColorPipe, FormGroupArrayPipe, SosSaveComponent, FastFrameComponent, SosNoteComponent, SosDealComponent, SosSettingsComponent, SosSupplyResetComponent, SosTransferChangeDatesComponent, SupplyPipelineContractsPipe]
})
export class SosComponent implements OnDestroy, AfterContentInit {
  @ViewChild('nomDate') nomDatePicker: DatePickerComponent;
  @ViewChild('transfersComponent') transfersComponent: SosTransfersComponent;
  @ViewChild('frozenColValues') frozenColValuesDiv: ElementRef<HTMLDivElement>;
  @ViewChild('normalColValues') normalColValuesDiv: ElementRef<HTMLDivElement>;
  @ViewChild('normalColsTopSection') normalColsTopSectionDiv: ElementRef<HTMLDivElement>;
  @ViewChildren('ptrPercentElems') ptrPercentElems: SosElemNumericInputComponent[];
  @HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
    this.handleSpecial(e);
  };
  @HostListener('window:beforeunload') canDeactivate(): Observable<boolean> | boolean {
    if (document.activeElement instanceof HTMLInputElement) {
      document.activeElement.blur();
      //call an empty setTimeout function for the blur to trigger a dirty form
      //this doesn't actually wait for the blur to happen, but it seems to be enough to work
      setTimeout(() => { }, 100);
    }
    //true will navigate without confirmation; false will show a confirm dialog before navigating away
    return !this.isGridDirty();
  }

  isGridDirty(): boolean {
    const mainViewDirty = this.itemForms.some(f => f.dirty);
    const transfersViewDirty = this.transfersComponent && this.transfersComponent.itemForms.some(f => f.dirty) ? true : false;
    if (mainViewDirty || transfersViewDirty)
      return true;
    else
      return false;
  }

  handleSpecial(e: KeyboardEvent): void {
    if (e.key === 'Escape' && document.activeElement instanceof HTMLElement)
      document.activeElement.blur();
  }

  util = util;
  icons = util.icons;
  models = models;
  hasModifyPermission = false;
  localRequiredData: RequiredData;
  prodDate: Date = util.currentDate.add(1, 'day').toDate();
  testDate1: Date = new Date(2025, 6, 14);
  nomDate: Date = null;
  pipeId: number = null;
  deliveryPointId: number = null;
  defaultNomDate = environment.production ? this.prodDate : this.testDate1;
  externalParams: ExternalParams;
  mainFrozenColCount: number;
  transferFrozenColCount: number;
  fontSizeStr: string;
  displays: { [key: string]: DisplayInfo };
  mainDisplayInfos: DisplayInfo[];
  transferDisplayInfos: DisplayInfo[];
  swingColCount = 0;
  baseloadColCount = 0;
  doc = document;
  loadedPipeId: number;
  loadedPointId: number;
  loadedNomDate: Date;
  sosItemsToSave: models.SosItem[];
  pipeName: string;
  loadForm: util.FormModel<SosLoadOptions>;
  dataSetForm: util.FormModel<models.SosDataSet>;
  itemFormArray: FormArray;
  itemForms: FormGroup[];
  marketFormArray: FormArray;
  marketForms: FormGroup[];
  transferColWidth = transferColWidth;
  marketColWidth = marketColWidth;
  dealUrl: string;
  totalTransferSumVol: number = 0;
  grandTotalDeliveryVol: number = 0;
  lastSelectedItem: models.SosItem;
  selectedNomForm: FormGroup;
  adjustedSettings: SosSettingItem;
  marketColDisplay: DisplayInfo = { displayName: '', isVisible: true, order: 99, propName: 'volume', width: marketColWidth, sortDir: null, sortNum: null }
  transferColDisplay: DisplayInfo = { displayName: '', isVisible: true, order: 99, propName: 'volume', width: transferColWidth, sortDir: null, sortNum: null }
  addDealParams: AddDealParams = null;
  mainShowHiddenRows: boolean = false;
  sosTipClass: string = '';
  mainGridSort: SortDescriptor[] = [];
  defaultSort: SortDescriptor[] = [{ field: 'supplyTicket', dir: 'asc' }, { field: 'receiptMeter', dir: 'asc' }];
  itemIndexesByGuid: Record<string, number>;
  marketIndexesByGuid: Record<string, number>;
  areCustomContextMenusEnabled: boolean = true;

  showMainTooltip = SosHelper.showMainTooltip;

  title$: Observable<string>;
  loading$ = new BehaviorSubject<boolean>(true);
  gridLoading$ = new BehaviorSubject<boolean>(false);
  screenPathsResult$: Observable<Dictionary<string>>;
  requiredData$: Observable<RequiredData>;
  result$: Observable<models.SosDataSet>;
  swingMarkets$: Observable<models.SosMarket[]>;
  baseloadMarkets$: Observable<models.SosMarket[]>;
  saveSelectedPointResult$: Observable<object>;
  setItemDisplayResult$: Observable<object>;
  saveSortResult$: Observable<object>;
  pipelineChanged$: Observable<number>;
  refreshPointsForPipe$ = new Subject<number>();
  pointsForPipeline$: Observable<models.PointInfo[]>;
  metersForPipeline$: Observable<models.MeterInfo[]>;
  pointChanged$: Observable<number>;
  nomDateChanged$: Observable<Date>;
  filterPipelines$ = new BehaviorSubject<string>(null)
  pipelines$: Observable<PipeInfo[]>;
  filterPoints$: BehaviorSubject<string>;
  points$: Observable<PointInfo[]>;
  filterMeters$: BehaviorSubject<string>;
  meters$: Observable<MeterInfo[]>;
  deliveryMeters$: Observable<util.IdName[]>;
  allPipeContracts$: Observable<SosPipeContractInfo[]>;
  allPtrContracts$: Observable<SosPipeContractInfo[]>;
  filterPipeContracts$: BehaviorSubject<string>;
  pipeContracts$: Observable<SosPipeContractInfo[]>;
  filterPtrContracts$: BehaviorSubject<string>;
  ptrContracts$: Observable<SosPipeContractInfo[]>;
  nomChangedUnsubscribe$ = new Subject();
  refreshDataSet$ = new Subject<boolean>()
  refreshRequiredData$ = new BehaviorSubject(util.RefreshType.WithOthers)
  dealsOpened$ = new BehaviorSubject<boolean>(false);
  snapshotHistoryOpened$ = new BehaviorSubject<boolean>(false);
  sosSettingsOpened$ = new BehaviorSubject<boolean>(false);
  addDealOpened$ = new BehaviorSubject<boolean>(false);
  noteOpened$ = new BehaviorSubject<boolean>(false);
  legendOpened$ = new BehaviorSubject<boolean>(false);
  transfersOpened$ = new BehaviorSubject<boolean>(false);
  saveSelectedPoint$ = new Subject<number>();
  setItemDisplay$ = new Subject<models.SosItem>();
  saveSort$ = new Subject();
  cloneSupply$ = new Subject<models.SosItem>();
  cloneSupplyResult$: Observable<object>;
  supplyResetOpened$ = new BehaviorSubject<boolean>(false);
  changeTransferDatesOpened$ = new BehaviorSubject<boolean>(false);
  saveNomsOpened$ = new BehaviorSubject<boolean>(false);
  marketDeliveryMeterUnsubscribe$ = new Subject();
  sourceNotesUnsubscribe$ = new Subject();
  activityNumUnsubscribe$ = new Subject();
  notesUnsubscribe$ = new Subject();
  pipeContractIdUnsubscribe$ = new Subject();
  ptrContractIdUnsubscribe$ = new Subject();
  ptrDeliveryMeterIdUnsubscribe$ = new Subject();
  ptrPercentUnsubscribe$ = new Subject();
  setHoveredItem$ = new BehaviorSubject<FormGroup>(null);
  hoveredItemResult$: Observable<string>;
  sortObserver$: Observable<object>;
  sortChanged$ = new Subject<string>;
  getItemDynamicValues$ = new Subject<FormGroup>();
  getItemDynamicValuesResult$: Observable<models.SosItem>;
  deleteTransfersResult$: Observable<object>
  deleteTransfers$ = new Subject<models.SosItem>();

  constructor(private messageService: MessageService, private titleService: Title, private service: SosService, private settingService: SosSettingsService, private transferService: SosTransfersService, private commonService: CommonService, private fb: CustomFormBuilder, private ref: ChangeDetectorRef, private dialogService: DialogService, private notify: NotifyService, private activatedRoute: ActivatedRoute) {
    this.loadForm = this.getLoadForm();
    this.dataSetForm = this.getDataSetForm();

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

    this.screenPathsResult$ = this.commonService.screenPaths$.pipe(
      tap(paths => {
        this.dealUrl = paths.get('deal');
      })
    );

    this.requiredData$ = this.refreshRequiredData$.pipe(
      tap(() => this.loading$.next(true)),
      switchMap(refreshType => {
        return combineLatest([this.service.requiredData$, of(refreshType)]);
      }),
      map(([requiredData,]) => {
        this.localRequiredData = requiredData;
        return requiredData;
      }),
      tap((requiredData) => {
        this.loading$.next(false);
        const hasAutoPointId = requiredData.autoSelectPointId > 0;
        const autoPipeId = hasAutoPointId ? requiredData.points.find(x => x.pointId === requiredData.autoSelectPointId)?.pipeId : undefined;
        if (autoPipeId) {
          const pointId = requiredData.autoSelectPointId;
          this.loadedPipeId = autoPipeId;
          this.loadedPointId = pointId;
          this.loadedNomDate = this.defaultNomDate;
          this.loadForm.patchValue({ pipeId: autoPipeId, pointId: pointId });
        }

        this.hasModifyPermission = requiredData.hasModifyPermission;
        util.focusAndSelectInputTarget();
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(3)
    );

    this.result$ = this.refreshDataSet$.pipe(
      filter(force => {
        this.loadForm.updateValueAndValidity({ emitEvent: false, onlySelf: true });
        const loadOps = this.loadForm.value as SosLoadOptions;
        const hasPointId = loadOps.pointId != null && loadOps.pointId > 0;
        const hasNomDate = loadOps.nomDate != null;
        const isAlreadyLoaded = loadOps.pointId == this.loadedPointId && dayjs(loadOps.nomDate).isSame(this.loadedNomDate, 'day');
        return hasPointId && hasNomDate && (!isAlreadyLoaded || force);
      }),
      tap(() => {
        this.gridLoading$.next(true);
        this.dataSetForm.disable();
      }),
      util.fastDebounce(500),
      switchMap(() => {
        const loadOps = this.loadForm.value as SosLoadOptions;
        return this.service.getDataSet(loadOps.pipeId, loadOps.pointId, loadOps.nomDate);
      }),
      map(result => {
        const loadOps = this.loadForm.value as SosLoadOptions;
        this.loadedPipeId = loadOps.pipeId;
        this.loadedPointId = loadOps.pointId;
        this.loadedNomDate = loadOps.nomDate;
        SosHelper.adjustPercents(result.items, AdjustType.ForDisplay);
        this.updateSettings(result.settings);
        this.removeHiddenSorts();
        result.items = this.getSortedItems(result);
        this.updateDisplays();
        this.setDataSetValue(result);
        return result;
      }),
      tap(() => {
        this.setSosLoadedTitle();
        this.dataSetForm.markAsPristine();
        this.dataSetForm.enable();
        this.disableTransferToTransfer();
        this.gridLoading$.next(false);
        this.setSelectedItem(null);
      }),
      shareReplay(1),
      catchError(err => {
        this.dataSetForm.enable();
        this.gridLoading$.next(false);
        return util.handleError(err, this.messageService);
      }), retry(10)
    );

    this.swingMarkets$ = this.result$.pipe(
      map(x => x.markets.filter(x => x.marketDealType === 'Swing')),
      tap(x => this.swingColCount = x.length)
    );

    this.baseloadMarkets$ = this.result$.pipe(
      map(x => x.markets.filter(x => x.marketDealType === 'Baseload')),
      tap(x => this.baseloadColCount = x.length)
    );

    this.saveSelectedPointResult$ = this.saveSelectedPoint$.pipe(
      switchMap(pointId => {
        return this.settingService.saveSelectedPoint(pointId);
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(10)
    );

    this.pipelineChanged$ = this.loadForm.get('pipeId').valueChanges.pipe(
      filter(() => {
        return !this.loading$.value && this.loadForm.get('pipeId').valid;
      }),
      tap(pipeId => {
        this.pipeName = this.localRequiredData.pipelines.find(x => x.pipeId === pipeId)?.pipeName ?? '';
        this.refreshPointsForPipe$.next(null);
      })
    );

    this.pointsForPipeline$ = this.refreshPointsForPipe$.pipe(
      map(() => {
        const newPipeId: number = this.loadForm.get('pipeId').getRawValue();
        let pointsForPipeline: models.PointInfo[] = null;
        if (newPipeId) {
          pointsForPipeline = this.localRequiredData.points.filter(pointInfo => {
            const isMappedItem = pointInfo.pipeId === newPipeId;
            return isMappedItem;
          });
        }

        return pointsForPipeline;
      }),
      tap(pointsForPipeline => {
        const loadOps = this.loadForm.value;

        if (!pointsForPipeline || pointsForPipeline.findIndex(pointInfo => pointInfo.pointId === loadOps.pointId) === -1)
          this.loadForm.patchValue({ pointId: null });

        if (!this.loading$.value && pointsForPipeline && pointsForPipeline.length === 1) {
          const newPointId = pointsForPipeline[0].pointId;
          this.loadForm.patchValue({ pointId: newPointId });
          this.saveSelectedPoint$.next(newPointId);
        }
      }),
      shareReplay(1)
    );

    this.metersForPipeline$ = this.pipelineChanged$.pipe(
      map(newPipeId => {
        const meters = this.localRequiredData.meters.filter(meter => meter.pipeId === newPipeId);
        return meters;
      }),
      shareReplay(1)
    );

    this.pointChanged$ = this.loadForm.get('pointId').valueChanges.pipe(
      filter(() => {
        return !this.loading$.value && this.loadForm.get('pointId').valid;
      }),
      tap((pointId: number) => {
        const cancelFunc = () => {
          this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId, nomDate: this.loadedNomDate }, { emitEvent: false });
        };

        const dontSaveFunc = () => {
          this.refresh();
          this.saveSelectedPoint$.next(pointId);
        };

        if (this.isGridDirty())
          util.onDetailChanging(this.itemForms, this.dialogService, dontSaveFunc, this.openSaveNoms, { fnCancelAction: cancelFunc, skipPristine: true });
        else
          dontSaveFunc();
      })
    );

    this.nomDateChanged$ = this.loadForm.get('nomDate').valueChanges.pipe(
      filter(() => {
        return !this.loading$.value && this.loadForm.get('nomDate').valid;
      }),
      tap(() => {
        const cancelFunc = () => {
          this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId, nomDate: this.loadedNomDate }, { emitEvent: false });
        };

        const dontSaveFunc = () => {
          this.refresh();
        };

        if (this.isGridDirty())
          util.onDetailChanging(this.itemForms, this.dialogService, dontSaveFunc, this.openSaveNoms, { fnCancelAction: cancelFunc, skipPristine: true });
        else
          dontSaveFunc();
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(1)
    );

    this.hoveredItemResult$ = this.setHoveredItem$.pipe(
      debounceTime(10),
      switchMap(form => {
        const itemForms = this.itemForms as FormGroup[];
        SosHelper.setHoveredItem(itemForms, form);
        return of(null);
      })
    );

    this.setItemDisplayResult$ = this.setItemDisplay$.pipe(
      switchMap(item => {
        return this.settingService.setItemDisplay(item, this.loadedPointId);
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(10)
    );

    this.saveSortResult$ = this.saveSort$.pipe(
      switchMap(() => {
        return this.settingService.saveSort(this.mainGridSort, this.loadedPipeId);
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      }), retry(10)
    );

    this.sortObserver$ = this.sortChanged$.pipe(
      tap(sortPropName => {
        this.gridLoading$.next(true);
        this.removeHiddenSorts();

        if (sortPropName === 'clear') {
          this.mainGridSort = [];
          this.saveSort$.next(null);
        }
        else if (sortPropName) {
          //if sortPropName exists in mainGridSort and the dir is asc, then change to desc
          //if sortPropName exists in mainGridSort and the dir is desc, then remove from mainGridSort
          //if sortPropName does not exist in mainGridSort, then add to mainGridSort with dir asc
          const sortIndex = this.mainGridSort.findIndex(x => x.field === sortPropName);
          if (sortIndex > -1) {
            if (this.mainGridSort[sortIndex].dir === 'asc')
              this.mainGridSort[sortIndex].dir = 'desc';
            else
              this.mainGridSort.splice(sortIndex, 1);
          }
          else
            this.mainGridSort.push({ field: sortPropName, dir: 'asc' });
          this.saveSort$.next(null);
        }
        //null sortPropName re-applies the existing sort which may be needed after a column is hidden
        this.updateDisplays();
      }),
      debounceTime(100),
      switchMap(() => {
        const dataSet = this.dataSetForm.getRawValue();
        dataSet.items = this.getSortedItems(dataSet);
        this.refreshFormArrays(dataSet);
        this.disableTransferToTransfer();
        return of(null);
      }),
      tap(() => {
        this.gridLoading$.next(false);
      }),
      catchError(err => {
        return util.handleError(err, this.messageService);
      })
    );

    //we group by guid so that we can debounce the calls to the server by guid
    //this is needed because the user can type in the grid and we don't want to call the server for every key stroke of an item
    //but we want to have a separate debounce for each item in case the user is typing in multiple different items rapidly
    this.getItemDynamicValuesResult$ = this.getItemDynamicValues$.pipe(
      groupBy(itemForm => itemForm.get('guid').value),
      mergeMap(group => group.pipe(
        debounceTime(500),
        switchMap(itemForm => {
          itemForm.patchValue({ isDynamicValueLoading: true }, { emitEvent: false });
          const sosItem = itemForm.getRawValue() as models.SosItem;
          SosHelper.adjustPercents([sosItem], AdjustType.ForSave);
          this.ref.detectChanges();
          return combineLatest([this.service.getItemDynamicValues(sosItem, this.loadedPipeId, this.loadedPointId, this.loadedNomDate), of(itemForm)]);
        }),
        map(([newItem, itemForm]) => {
          SosHelper.adjustPercents([newItem], AdjustType.ForDisplay);

          itemForm.patchValue({
            isDynamicValueLoading: false,
            nomFuelPercent: newItem.nomFuelPercent,
            nomFuelAmount: newItem.nomFuelAmount,
            ptrAmount: newItem.ptrAmount,
            ptrFuelPercent: newItem.ptrFuelPercent,
            ptrFuelAmount: newItem.ptrFuelAmount,
            ptrReceiptVol: newItem.ptrReceiptVol,
            nomReceiptVol: newItem.nomReceiptVol,
            totalReceiptVol: newItem.totalReceiptVol,
            otherUnscheduledVol: newItem.otherUnscheduledVol,
            unscheduledVolume: newItem.unscheduledVolume,
            otherCumReceiptVol: newItem.otherCumReceiptVol,
            cumReceiptVol: newItem.cumReceiptVol
          }, { emitEvent: false });

          //if there are ptrPercentElems that have isActive true, then we don't want to update the ptrPercent
          if (this.ptrPercentElems.length > 0 && this.ptrPercentElems.findIndex(x => x.isActive) === -1)
            itemForm.patchValue({ ptrPercent: newItem.ptrPercent }, { emitEvent: false })

          return newItem;
        }),
        shareReplay(1),
        catchError(err => {
          return util.handleError(err, this.messageService);
        })
      ))
    );

    this.deleteTransfersResult$ = this.deleteTransfers$.pipe(
      tap(() => {
        this.gridLoading$.next(true);
        this.dataSetForm.disable();
      }),
      switchMap(sosItem => {
        return this.transferService.deleteTransfersInPath(sosItem, this.loadedNomDate, sosItem.transferDealId);
      }),
      tap(() => {
        this.notify.success('transfers deleted');
        this.refresh();
      }),
      shareReplay(1),
      catchError(err => {
        this.dataSetForm.enable();
        this.gridLoading$.next(false);
        return util.handleError(err, this.messageService);
      })
    )

    this.cloneSupplyResult$ = this.cloneSupply$.pipe(
      switchMap(sosItem => {
        return combineLatest([of(sosItem), this.service.cloneSupply(sosItem)]);
      }),
      tap(() => {
        this.notify.success('supply cloned');
        this.refresh();
      }),
      shareReplay(1),
      catchError(err => {
        return util.handleError(err, this.messageService);
      })
    );

    this.filterPipelines$ = new BehaviorSubject<string>(null);
    this.pipelines$ = this.filterPipelines$.pipe(util.filterSpecials(this.loading$, this.requiredData$, 'pipelines', 'pipeName'));

    this.filterPoints$ = new BehaviorSubject<string>(null);
    this.points$ = this.filterPoints$.pipe(util.filterSpecials(this.loading$, this.pointsForPipeline$, null, 'pointName'));

    this.filterMeters$ = new BehaviorSubject<string>(null);
    this.meters$ = this.filterMeters$.pipe(util.filterSpecials(this.loading$, this.metersForPipeline$, null, 'meterNameAndNum'));

    this.deliveryMeters$ = this.result$.pipe(map(x => x.deliveryMeters));

    this.allPipeContracts$ = this.result$.pipe(map(x => x.pipeContracts));
    this.allPtrContracts$ = this.allPipeContracts$.pipe(map(x => x.filter(x => x.isPtrContract)));

    this.filterPipeContracts$ = new BehaviorSubject<string>(null);
    this.pipeContracts$ = this.filterPipeContracts$.pipe(util.filterSpecials(this.loading$, this.allPipeContracts$, null, 'contractName'));

    this.filterPtrContracts$ = new BehaviorSubject<string>(null);
    this.ptrContracts$ = this.filterPtrContracts$.pipe(util.filterSpecials(this.loading$, this.allPtrContracts$, null, 'contractName'));
  }

  ngAfterContentInit(): void {
    this.activatedRoute.queryParams.subscribe(params => {
      const newTabNomDate: string = params['newTab'];
      if (newTabNomDate) {
        const nomDate = dayjs(parseInt(newTabNomDate)).toDate();
        const emitEvent = this.loadedPointId != null;
        this.loadForm.patchValue({ nomDate: nomDate }, { emitEvent: emitEvent });
      }
      //if there is no nom date then refresh() will be called by pointchanged$ afer the pointId is set by refreshRequiredData$
    });
  }

  ngOnDestroy(): void {
    this.nomChangedUnsubscribe$.next(null);
    this.nomChangedUnsubscribe$.complete();
  }

  getLoadForm() {
    const fb = this.fb;
    const fg: util.FormModel<models.SosLoadOptions> = this.fb.group({
      pipeId: fb.ctr(null, Validators.required),
      pointId: fb.ctr(null, Validators.required),
      nomDate: fb.ctr(this.defaultNomDate, { validators: [Validators.required], updateOn: 'blur' })
    });

    return fg;
  }

  getDataSetForm() {
    const fb = this.fb;
    const fg: util.FormModel<models.SosDataSet> = fb.group({
      items: fb.arr([]),
      markets: fb.arr([]),
      settings: fb.ctr(null),
      deliveryMeters: fb.ctr(null), //not used in form
      pipeContracts: fb.ctr(null) //not used in form
    });

    this.itemFormArray = fg.get('items') as FormArray;
    this.itemForms = this.itemFormArray.controls as FormGroup[];

    this.marketFormArray = fg.get('markets') as FormArray;
    this.marketForms = this.marketFormArray.controls as FormGroup[];

    return fg;
  }

  getMarketForm(sosMarket: models.SosMarket) {
    const fb = this.fb;
    const fg: util.FormModel<models.SosMarket> = this.fb.group({
      guid: fb.ctr(sosMarket ? sosMarket.guid : null, Validators.required),
      marketTicket: fb.ctr(sosMarket ? sosMarket.marketTicket : null, Validators.required),
      marketDealType: fb.ctr(sosMarket ? sosMarket.marketDealType : null, Validators.required),
      marketDeliveryMeterId: fb.ctr(sosMarket ? sosMarket.marketDeliveryMeterId : null, Validators.required),
      marketCounterparty: fb.ctr(sosMarket ? sosMarket.marketCounterparty : null, Validators.required),
      marketCounterpartyId: fb.ctr(sosMarket ? sosMarket.marketCounterpartyId : null, Validators.required),
      marketDealVolume: fb.ctr(sosMarket ? sosMarket.marketDealVolume : null, Validators.required),
      totalMarketNomVol: fb.ctr(sosMarket ? sosMarket.totalMarketNomVol : null, Validators.required),
      marketNomId: fb.ctr(sosMarket ? sosMarket.marketNomId : null, Validators.required),
      marketDealId: fb.ctr(sosMarket ? sosMarket.marketDealId : null),
      contractNumber: fb.ctr(sosMarket ? sosMarket.contractNumber : null),
      pointId: fb.ctr(sosMarket ? sosMarket.pointId : null),
      pointName: fb.ctr(sosMarket ? sosMarket.pointName : null)
    });

    return fg;
  }

  getItemForm(sosItem: models.SosItem) {
    const fb = this.fb;
    const fg: util.FormModel<models.SosItem> = this.fb.group({
      rowNum: fb.ctr(sosItem ? sosItem.rowNum : null),
      guid: fb.ctr(sosItem ? sosItem.guid : null, Validators.required),
      supplyCounterparty: fb.ctr(sosItem ? sosItem.supplyCounterparty : null, Validators.required),
      ownership: fb.ctr(sosItem ? sosItem.ownership : null),
      sourceMeter: fb.ctr(sosItem ? sosItem.sourceMeter : null, Validators.required),
      receiptMeter: fb.ctr(sosItem ? sosItem.receiptMeter : null, Validators.required),
      receiptPoint: fb.ctr(sosItem ? sosItem.receiptPoint : null),
      ptrContractId: fb.ctr(sosItem ? sosItem.ptrContractId : null),
      supplyTicket: fb.ctr(sosItem ? sosItem.supplyTicket : null, Validators.required),
      sourceTicket: fb.ctr(sosItem ? sosItem.sourceTicket : null, Validators.required),
      activityNum: fb.ctr(sosItem ? sosItem.activityNum : null),
      notes: fb.ctr(sosItem ? sosItem.notes : null),
      sourceNotes: fb.ctr(sosItem ? sosItem.sourceNotes : null),
      dealVolume: fb.ctr(sosItem ? sosItem.dealVolume : null, Validators.required),
      unscheduledVolume: fb.ctr(sosItem ? sosItem.unscheduledVolume : null, Validators.required),
      totalReceiptVol: fb.ctr(sosItem ? sosItem.totalReceiptVol : null, Validators.required),
      nomReceiptVol: fb.ctr(sosItem ? sosItem.nomReceiptVol : null, Validators.required),
      ptrReceiptVol: fb.ctr(sosItem ? sosItem.ptrReceiptVol : null, Validators.required),
      nomFuelPercent: fb.ctr(sosItem ? sosItem.nomFuelPercent : null),
      nomFuelAmount: fb.ctr(sosItem ? sosItem.nomFuelAmount : null),
      ptrFuelPercent: fb.ctr(sosItem ? sosItem.ptrFuelPercent : null),
      ptrFuelAmount: fb.ctr(sosItem ? sosItem.ptrPercent : null),
      ptrPercent: fb.ctr(sosItem ? sosItem.ptrPercent : null),
      ptrAmount: fb.ctr(sosItem ? sosItem.ptrAmount : null),
      deliveryVol: fb.ctr(sosItem ? sosItem.deliveryVol : null, Validators.required),
      cumReceiptVol: fb.ctr(sosItem ? sosItem.cumReceiptVol : null, Validators.required),
      pipeContractId: fb.ctr(sosItem ? sosItem.pipeContractId : null),
      dealType: fb.ctr(sosItem ? sosItem.dealType : null, Validators.required),
      deliveryPoint: fb.ctr(sosItem ? sosItem.deliveryPoint : null, Validators.required),
      ptrDeliveryMeterId: fb.ctr(sosItem ? sosItem.ptrDeliveryMeterId : null),
      transferSumVol: fb.ctr(sosItem ? sosItem.transferSumVol : null),
      noms: fb.arr([]),
      modifiedProps: fb.ctr(sosItem ? sosItem.modifiedProps : null),
      otherUnscheduledVol: fb.ctr(sosItem ? sosItem.otherUnscheduledVol : null),
      otherCumReceiptVol: fb.ctr(sosItem ? sosItem.otherCumReceiptVol : null),
      supplyNomId: fb.ctr(sosItem ? sosItem.supplyNomId : null, Validators.required),
      dealId: fb.ctr(sosItem ? sosItem.dealId : null),
      receiptPointId: fb.ctr(sosItem ? sosItem.receiptPointId : null),
      receiptMeterId: fb.ctr(sosItem ? sosItem.receiptMeterId : null, Validators.required),
      transferDealId: fb.ctr(sosItem ? sosItem.transferDealId : null),
      sourceMeterId: fb.ctr(sosItem ? sosItem.sourceMeterId : null),
      sourceMeterIds: fb.ctr(sosItem ? sosItem.sourceMeterIds : null),
      supplyCounterpartyId: fb.ctr(sosItem ? sosItem.supplyCounterpartyId : null),
      isManualPtr: fb.ctr(sosItem ? sosItem.isManualPtr : false),
      sourceDealVolume: fb.ctr(sosItem ? sosItem.sourceDealVolume : null),
      sourceDealProduct: fb.ctr(sosItem ? sosItem.sourceDealProduct : null),
      isEnteredFromSos: fb.ctr(sosItem ? sosItem.isEnteredFromSos : false),
      isHidden: fb.ctr(sosItem ? sosItem.isHidden : false),
      isSelected: fb.ctr(sosItem ? sosItem.isSelected : false),
      isHovered: fb.ctr(sosItem ? sosItem.isHovered : false),
      isDynamicValueLoading: fb.ctr(sosItem ? sosItem.isDynamicValueLoading : false),
      isTransferToTransfer: fb.ctr(sosItem ? sosItem.isTransferToTransfer : false),
      isSupplyClone: fb.ctr(sosItem ? sosItem.isSupplyClone : false),
      createdTime: fb.ctr(sosItem ? sosItem.createdTime : null),
      savedTime: fb.ctr(sosItem ? sosItem.savedTime : null)
    });

    const nomFormArray = fg.get('noms') as FormArray;
    nomFormArray.clear();
    sosItem.noms.forEach(nom => {
      nomFormArray.push(this.getNomFormGroup(nom));
    });

    return fg;
  }

  getNomFormGroup(nom: models.SosNom) {
    const fb = this.fb;
    const fg: util.FormModel<models.SosNom> = fb.group({
      volume: fb.ctr(nom ? nom.volume : null),
      notes: fb.ctr(nom ? nom.notes : null),
      isKeepWhole: fb.ctr(nom ? nom.isKeepWhole : null, Validators.required),
      marketTicket: fb.ctr(nom ? nom.marketTicket : null, Validators.required),
      marketDealType: fb.ctr(nom ? nom.marketDealType : null, Validators.required),
      marketDeliveryMeterId: fb.ctr(nom ? nom.marketDeliveryMeterId : null, Validators.required),
      marketDealId: fb.ctr(nom ? nom.marketDealId : null),
      isModified: fb.ctr(nom ? nom.isModified : null)
    });
    return fg;
  }

  openNewTab() {
    this.loadForm.updateValueAndValidity({ emitEvent: false, onlySelf: true });
    const loadOps = this.loadForm.value as SosLoadOptions;
    const hasNomDate = loadOps.nomDate != null;
    //if hasNomDate then open new tab with a query string of the nomDate in milliseconds
    if (hasNomDate)
      window.open('/#/SOS?newTab=' + dayjs(loadOps.nomDate).valueOf(), '_blank');
    else
      window.open('/#/SOS', '_blank');
  }

  prevDate() {
    if (this.loadForm.get('nomDate').valid) {
      const nomDate = dayjs(this.loadForm.value.nomDate as Date).add(-1, 'day').toDate()
      this.loadForm.patchValue({ nomDate: nomDate });
    }
  }

  nextDate() {
    if (this.loadForm.get('nomDate').valid) {
      const nomDate = dayjs(this.loadForm.value.nomDate as Date).add(1, 'day').toDate();
      this.loadForm.patchValue({ nomDate: nomDate });
    }
  }

  openSupplyDealOrTransfer(clickedDealOrTransferNum: string, item: models.SosItem) {
    if (util.isNullOrWhitespace(clickedDealOrTransferNum))
      return;

    const isNormalDeal = clickedDealOrTransferNum.substring(0, 3) === 'GAS';
    if (isNormalDeal)
      this.openDeal(clickedDealOrTransferNum);
    else
      this.openTransfer(item);
  }

  openDeal(dealNum: string): void {
    this.externalParams = { topless: 1, dealNum: dealNum };
    this.dealsOpened$.next(true);
  }

  openTransfer(item: models.SosItem): void {
    if (item.transferSumVol !== null) {
      this.lastSelectedItem = item;
      this.transfersOpened$.next(true);
    }
  }

  openSosSettings(): void {
    this.sosSettingsOpened$.next(true);
  }

  sosSettingsClosed(savedSettings: SosSettingItem): void {
    this.sosSettingsOpened$.next(false);
    if (savedSettings) {
      this.updateSettings(savedSettings);
      this.sortChanged$.next(null);
    }
  }

  openAddDeal(addDealParams: AddDealParams): void {
    this.addDealParams = addDealParams;

    const cancelFunc = () => {
      this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId, nomDate: this.loadedNomDate }, { emitEvent: false });
    };

    const dontSaveFunc = () => {
      this.addDealOpened$.next(true);
    };

    if (this.isGridDirty())
      util.onDetailChanging(this.itemForms, this.dialogService, dontSaveFunc, this.openSaveNoms, { fnCancelAction: cancelFunc, skipPristine: true });
    else
      dontSaveFunc();
  }

  openSaveNoms = () => {
    this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId, nomDate: this.loadedNomDate }, { emitEvent: false });
    const isDirty = this.itemForms.some(f => f.dirty);
    const allMarketNomsHaveMeters = this.checkIfMarketNomsHaveMeters();
    const sourceNotesHasMissingPipelineContracts = this.checkIfSourceNotesHasMissingPipelineContracts();

    if (isDirty && !allMarketNomsHaveMeters)
      this.notify.error('please select a market delivery meter');
    else if (isDirty && sourceNotesHasMissingPipelineContracts)
      this.notify.error('source notes require a pipeline contract');
    else if (isDirty) {
      this.setSelectedItem(null);
      this.saveNomsOpened$.next(true);
    }
    else
      this.notify.warning('no changes detected');
  }

  openSnapshotHistory(): void {
    this.nomDate = this.loadedNomDate
    this.pipeId = this.loadedPipeId
    this.deliveryPointId = this.loadedPointId
    this.snapshotHistoryOpened$.next(true);
  }

  closeSnapshotHistory(): void {
    this.snapshotHistoryOpened$.next(false);
  }

  checkIfMarketNomsHaveMeters(): boolean {
    let allMarketNomsHaveMeters = true;
    this.marketFormArray.controls.forEach((marketForm, marketIdx) => {
      const hasDeliveryMeter = marketForm.get('marketDeliveryMeterId').value !== null;
      const hasAnyNoms = this.itemForms.some(itemForm => {
        const item: models.SosItem = itemForm.getRawValue();
        const hasNom = item.noms[marketIdx]?.volume !== null;
        return hasNom;
      });
      if (hasAnyNoms && !hasDeliveryMeter)
        allMarketNomsHaveMeters = false;
    });
    return allMarketNomsHaveMeters;
  };

  private checkIfSourceNotesHasMissingPipelineContracts(): boolean {
    const isInvalid = this.itemForms.some(itemForm => {
      const item: models.SosItem = itemForm.getRawValue();
      const hasSourceNotes = item.sourceNotes != null && item.sourceNotes !== '';
      const isMissingPipelineContract = item.pipeContractId == null;
      return hasSourceNotes && isMissingPipelineContract;
    });
    return isInvalid;
  };

  addDealClosed(isDealAdded: boolean): void {
    this.addDealOpened$.next(false);
    if (isDealAdded) {
      this.notify.success('deal added');
      this.refreshDataSet$.next(true);
    }
  }

  transfersClosed(isTransferSaved: boolean): void {
    this.transfersOpened$.next(false);
    if (isTransferSaved) {
      this.refreshDataSet$.next(true);
    }
  }

  updateSettings(newSettings: SosSettingItem) {
    this.adjustedSettings = SosSettingsComponent.getAdjustedSettings(newSettings);
    this.fontSizeStr = this.adjustedSettings.fontSize + 'px';
    this.mainFrozenColCount = this.adjustedSettings.mainFrozenColumnCount;
    this.transferFrozenColCount = this.adjustedSettings.transferFrozenColumnCount;
    this.mainDisplayInfos = this.adjustedSettings.mainDisplayInfos;
    this.transferDisplayInfos = this.adjustedSettings.transferDisplayInfos;
    this.sosTipClass = 'app-sosTip-' + newSettings.fontSize;
    this.mainGridSort = this.adjustedSettings.sorts;
  }

  updateRowNums(items: models.SosItem[] = null) {
    let rowNum: number = 1;

    if (items === null) {
      this.itemForms.forEach((itemForm) => {
        if (this.mainShowHiddenRows || !itemForm.value.isHidden) {
          itemForm.patchValue({ rowNum: rowNum }, { emitEvent: false });
          rowNum++;
        }
        else
          itemForm.patchValue({ rowNum: null }, { emitEvent: false });
      });
    }
    else {
      items.forEach((item) => {
        if (this.mainShowHiddenRows || !item.isHidden) {
          item.rowNum = rowNum;
          rowNum++;
        }
        else
          item.rowNum = null;
      });
    }
  }

  onPipePointKeydown(event: KeyboardEvent): void {
    if (event.key === "Escape" && this.loadedPipeId && this.loadedPointId)
      this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId });
  }

  onNomDatePickerClose() {
    setTimeout(() => {
      this.nomDatePicker.blur();
    });
  }

  saveNomsClosed(areNomsSaved: boolean) {
    this.saveNomsOpened$.next(false);
    if (areNomsSaved) {
      this.notify.success('nominations saved');
      this.refreshDataSet$.next(true);
    }
  }

  supplyResetClosed(wasSupplyReset: boolean) {
    this.supplyResetOpened$.next(false);
    if (wasSupplyReset) {
      this.notify.success('supply reset');
      this.refreshDataSet$.next(true);
    }
  }

  changeTransferDatesClosed(wereTransferDatesChanged: boolean) {
    this.changeTransferDatesOpened$.next(false);
    if (wereTransferDatesChanged) {
      this.notify.success('transfer changes successful');
      this.refreshDataSet$.next(true);
    }
  }

  refreshSelected() {
    const cancelFunc = () => {
      this.loadForm.patchValue({ pipeId: this.loadedPipeId, pointId: this.loadedPointId, nomDate: this.loadedNomDate }, { emitEvent: false });
    };

    const dontSaveFunc = () => {
      this.refresh();
    };

    if (this.isGridDirty())
      util.onDetailChanging(this.itemForms, this.dialogService, dontSaveFunc, this.openSaveNoms, { fnCancelAction: cancelFunc, skipPristine: true });
    else
      dontSaveFunc();
  }

  private refresh() {
    this.refreshDataSet$.next(true);
  }

  refreshFormArrays(dataSet: models.SosDataSet) {
    this.updateRowNums(dataSet.items);

    const updatedItemForms: FormGroup[] = [];
    const updatedMarketForms: FormGroup[] = [];

    // Update existing items and add new items to itemFormArray
    const existingItemGuids = new Set<string>();
    dataSet.items.forEach(item => {
      let itemForm = this.itemFormArray.controls.find(control => control.value.guid === item.guid) as FormGroup;
      if (itemForm) {
        itemForm.setValue(item);
      } else {
        itemForm = this.getItemForm(item);
        this.itemFormArray.push(itemForm);
      }
      existingItemGuids.add(item.guid);
      updatedItemForms.push(itemForm);
    });

    // Remove items from itemFormArray that are not in dataSet.items
    for (let i = this.itemFormArray.length - 1; i >= 0; i--) {
      const itemForm = this.itemFormArray.at(i);
      if (!existingItemGuids.has(itemForm.value.guid)) {
        this.itemFormArray.removeAt(i);
      }
    }

    // Update existing markets and add new markets to marketFormArray
    const existingMarketGuids = new Set<string>();
    dataSet.markets.forEach(market => {
      let marketForm = this.marketFormArray.controls.find(control => control.value.guid === market.guid) as FormGroup;
      if (marketForm) {
        marketForm.setValue(market);
      } else {
        marketForm = this.getMarketForm(market);
        this.marketFormArray.push(marketForm);
      }
      existingMarketGuids.add(market.guid);
      updatedMarketForms.push(marketForm);
    });

    // Remove markets from marketFormArray that are not in dataSet.markets
    for (let i = this.marketFormArray.length - 1; i >= 0; i--) {
      const marketForm = this.marketFormArray.at(i);
      if (!existingMarketGuids.has(marketForm.value.guid))
        this.marketFormArray.removeAt(i);
    }

    //this is used so that we can reference the correct item in the formArray after sorting
    this.itemIndexesByGuid = {};
    for (let i = this.itemFormArray.length - 1; i >= 0; i--) {
      const itemForm = this.itemFormArray.at(i);
      this.itemIndexesByGuid[itemForm.value.guid] = i;
    }

    //this is used so that we can reference the correct item in the formArray after new markets
    this.marketIndexesByGuid = {};
    for (let i = this.marketFormArray.length - 1; i >= 0; i--) {
      const marketForm = this.marketFormArray.at(i);
      this.marketIndexesByGuid[marketForm.value.guid] = i;
    }

    //sorting directly on the formArray throws an error
    //repatching the formArray with the sorted items would work but is slow
    //so we sort the itemForms and marketForms collections instead since that is faster
    this.itemForms = updatedItemForms;
    this.marketForms = updatedMarketForms;
  }

  rewatch() {
    setTimeout(() => {
      util.watchForFormArrayChanges(this.marketForms, 'marketDeliveryMeterId', this.marketDeliveryMeterUnsubscribe$, this.marketDeliveryMeterChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'sourceNotes', this.sourceNotesUnsubscribe$, this.sourceNotesChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'activityNum', this.activityNumUnsubscribe$, this.supplyPropChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'notes', this.notesUnsubscribe$, this.supplyPropChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'pipeContractId', this.pipeContractIdUnsubscribe$, this.supplyPropChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'ptrContractId', this.ptrContractIdUnsubscribe$, this.supplyPropChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'ptrDeliveryMeterId', this.ptrDeliveryMeterIdUnsubscribe$, this.supplyPropChanged, this.gridLoading$);
      util.watchForFormArrayChanges(this.itemForms, 'ptrPercent', this.ptrPercentUnsubscribe$, this.supplyPropChanged, this.gridLoading$);

      //first unsbuscribe from all the previous subscriptions
      this.nomChangedUnsubscribe$.next(null);

      this.nomChangedUnsubscribe$.complete();
      this.itemForms.forEach(itemForm => {
        const nomFormArray = itemForm.get('noms') as FormArray;
        const nomForms = nomFormArray.controls as FormGroup[];
        nomForms.forEach(nomForm => {
          nomForm.valueChanges.pipe(
            filter(() => {
              return !this.gridLoading$.getValue();
            }),
            takeUntil(this.nomChangedUnsubscribe$),
          ).subscribe(() => {
            //disabled volumes should never be set to modified; usually they are TransferToTransfer noms which shouldn't have a volume
            if (!nomForm.controls['volume'].disabled) {
              nomForm.patchValue({ isModified: true }, { emitEvent: false });
              this.calcTotalMarketNomVols();
              this.calcSingleDeliveryVol(itemForm);
              this.calcGrandTotalDeliveryVol();
              this.getItemDynamicValues$.next(itemForm);
            }
          });
        });
      });
    }, 0);
  }

  //if there are more than one transfer that come from the same supply source ticket and receipt meter, then the source notes should be the same
  sourceNotesChanged = (change: util.FormArrayChange) => {
    if (this instanceof SosComponent) {
      const changingItem = change.form.getRawValue() as models.SosItem;
      const newSourceNotes = change.value;
      const itemForms = this.itemForms as FormGroup[];
      itemForms.forEach(otherItemForm => {
        const otherItem: models.SosItem = otherItemForm.getRawValue();
        const isSameSupplyNomId = otherItem.supplyNomId === changingItem.supplyNomId;
        const isSameSourceTicket = otherItem.sourceTicket === changingItem.sourceTicket;
        const isSameSourceMeterId = otherItem.sourceMeterId === changingItem.sourceMeterId;
        if (!isSameSupplyNomId && isSameSourceTicket && isSameSourceMeterId) {
          otherItemForm.patchValue({ sourceNotes: newSourceNotes }, { emitEvent: false });
          this.patchModifiedProps(otherItemForm, change.formFieldName);
        }
        this.patchModifiedProps(change.form, change.formFieldName);
      });
    }
  }

  //if a market delivery meter changes, then the delivery meter should be updated for all the market's nominations
  marketDeliveryMeterChanged = (change: util.FormArrayChange) => {
    if (this instanceof SosComponent) {
      const changingItem = change.form.getRawValue() as models.SosMarket;
      const newDeliveryMeterId = change.value;
      const itemForms = this.itemForms as FormGroup[];
      itemForms.forEach(itemForm => {
        const nomsFormArray = itemForm.get('noms') as FormArray;
        nomsFormArray.controls.forEach((control) => {
          const nomForm = control as FormGroup;
          const nom = nomForm.getRawValue() as models.SosNom;
          if (nom.marketDealId === changingItem.marketDealId) {
            //disabled volumes should never be set to modified; usually they are TransferToTransfer noms which shouldn't have a volume
            //but we still need to update their meter id so that the save function doesn't get confused about how many market pairs there are
            //for all other enabled volumes, we set isModified to true so that the save function knows to update them
            //regardless of whether the volume has changed or not, since the nom meter needs to change
            const isModified = nomForm.controls['volume'].disabled ? false : true;
            nomForm.patchValue({ marketDeliveryMeterId: newDeliveryMeterId, isModified: isModified }, { emitEvent: false });
            nomForm.markAsDirty();
          }
        });
      });
    }
  }

  supplyPropChanged = (change: util.FormArrayChange) => {
    if (this instanceof SosComponent) {
      this.patchModifiedProps(change.form, change.formFieldName);

      const dynamicSupplyDependents = ['pipeContractId', 'ptrContractId', 'ptrDeliveryMeterId', 'ptrPercent'];
      if (dynamicSupplyDependents.includes(change.formFieldName))
        this.getItemDynamicValues$.next(change.form);
    }
  }

  setDataSetValue(dataSet: models.SosDataSet) {
    util.convertToDates(dataSet);
    this.setClientOnlyProps(dataSet);
    this.refreshFormArrays(dataSet);
    this.dataSetForm.setValue(dataSet);
    this.calcTotalTransferSumVol(dataSet);
    this.calcTotalMarketNomVols();
    this.calcAllRowDeliveryVols();
    this.calcGrandTotalDeliveryVol();
    this.rewatch();
  }

  disableTransferToTransfer() {
    this.itemForms.forEach(itemForm => {
      const sosItem = itemForm.value as models.SosItem;
      const isTransferToTransfer = sosItem.isTransferToTransfer;
      const nomFormArray = itemForm.get('noms') as FormArray;
      const nomForms = nomFormArray.controls as FormGroup[];
      nomForms.forEach(nomForm => {
        if (isTransferToTransfer)
          nomForm.disable();
        else
          nomForm.enable();
      });
    });
  }

  calcTotalMarketNomVols() {
    this.marketFormArray.controls.forEach((marketForm, marketIdx) => {
      let total = 0;
      this.itemForms.forEach(itemForm => {
        const item: models.SosItem = itemForm.getRawValue();
        const nomVol = item.noms[marketIdx]?.volume ?? 0;
        total += nomVol;
      });
      marketForm.patchValue({ totalMarketNomVol: total });
    });
  }

  calcTotalTransferSumVol(dataSet: models.SosDataSet) {
    let total = 0;
    dataSet.items.forEach(item => {
      total += item.transferSumVol;
    });
    this.totalTransferSumVol = total;
  }

  calcSingleDeliveryVol(itemForm: FormGroup) {
    const item: models.SosItem = itemForm.getRawValue();
    const nomTotal = item.noms.reduce((acc, x) => acc + (x.volume ?? 0), 0);
    itemForm.patchValue({ deliveryVol: nomTotal });
  }

  calcAllRowDeliveryVols() {
    const itemForms = this.itemForms as FormGroup[];
    itemForms.forEach(itemForm => {
      this.calcSingleDeliveryVol(itemForm);
    });
  }

  calcGrandTotalDeliveryVol() {
    let grandTotal = 0;
    const itemForms = this.itemForms as FormGroup[];
    itemForms.forEach(itemForm => {
      const item: models.SosItem = itemForm.getRawValue();
      grandTotal += item.deliveryVol;
    });
    this.grandTotalDeliveryVol = grandTotal;
  }

  setSosLoadedTitle() {
    const pipeShort = this.localRequiredData.pipelines.find(x => x.pipeId === this.loadedPipeId)?.pipeShort ?? '';
    const pointName = this.localRequiredData.points.find(x => x.pointId === this.loadedPointId)?.pointName;
    let title = `${pipeShort}`;
    if (pointName) {
      title += ` / ${pointName}`;
    }
    util.trySetTitle(this.titleService, title);
  }

  patchModifiedProps(form: FormGroup, propName: string) {
    const modifiedProps: string[] = form.get('modifiedProps').value;
    if (!modifiedProps.includes(propName))
      modifiedProps.push(propName);
    form.patchValue({ modifiedProps: modifiedProps }, { emitEvent: false });
  }

  onContextMenuSelect(e: ContextMenuSelectEvent, item: models.SosItem, nomForm: FormGroup) {
    const contextMenuItem: models.SosContextItem = e.item;
    const itemForms = this.itemForms as FormGroup[];
    const itemForm = itemForms.find(x => x.value.guid === item.guid);
    this.selectedNomForm = nomForm;
    const addDealParams: AddDealParams = {
      selectedDrawerText: 'Supply Deal',
      supplyCounterpartyId: item.supplyCounterpartyId,
      supplyDealType: item.dealType,
      receiptPointId: item.receiptPointId,
      marketCounterpartyId: null,
      marketDealType: 'Swing',
      transferSourceCounterpartyId: null,
      transferSourceMeterId: null,
      sourceTicket: item.sourceTicket
    };

    const selectedItemText = contextMenuItem.menuOption.text;
    if (selectedItemText === models.SosContextMenuOptions.ShowRow.text)
      this.showRow(itemForm, item);
    else if (selectedItemText === models.SosContextMenuOptions.HideRow.text)
      this.hideRow(itemForm, item);
    else if (selectedItemText === models.SosContextMenuOptions.KeepWhole.text)
      this.toggleKeepWhole(nomForm);
    else if (selectedItemText === models.SosContextMenuOptions.EditNomNote.text)
      this.showNote();
    else if (selectedItemText === models.SosContextMenuOptions.AddSupplyDeal.text) {
      addDealParams.selectedDrawerText = 'Supply Deal';
      this.openAddDeal(addDealParams);
    }
    else if (selectedItemText === models.SosContextMenuOptions.AddMarketDeal.text) {
      const nomValue: models.SosNom = nomForm.value;
      const marketItem: models.SosMarket = this.marketFormArray.controls.find(x => x.value.marketDealId === nomValue.marketDealId).value;
      addDealParams.selectedDrawerText = 'Market Deal';
      addDealParams.marketCounterpartyId = marketItem.marketCounterpartyId;
      addDealParams.marketDealType = marketItem.marketDealType;
      this.openAddDeal(addDealParams);
    }
    else if (selectedItemText === models.SosContextMenuOptions.AddTransferDeal.text) {
      addDealParams.selectedDrawerText = 'Transfer Deal';
      addDealParams.transferSourceMeterId = item.receiptMeterId;
      addDealParams.transferSourceCounterpartyId = item.supplyCounterpartyId;
      this.openAddDeal(addDealParams);
    }
    else if (selectedItemText === models.SosContextMenuOptions.CloneSupplyRow.text)
      this.cloneSupplyRow(item);
    else if (selectedItemText === models.SosContextMenuOptions.ResetSupplyRow.text)
      this.resetSupplyRow();
    else if (selectedItemText === models.SosContextMenuOptions.DeleteTransfersInPath.text)
      this.deleteTransfersInPath(item);
    else if (selectedItemText === models.SosContextMenuOptions.ChangeTransferDates.text)
      this.changeTransferDates();
  }

  deleteTransfersInPath(item: models.SosItem) {
    const dontSaveFunc = () => {
      const deleteConfirmSettings: DialogSettings = {
        title: "Please confirm",
        content: "Are you sure you want to delete all the transfers in this path?",
        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.deleteTransfers$.next(item);
      });
    };

    if (this.isGridDirty())
      util.onDetailChanging(this.itemForms, this.dialogService, dontSaveFunc, this.openSaveNoms, { skipPristine: true });
    else
      dontSaveFunc();
  }

  showRow(itemForm: FormGroup, item: models.SosItem) {
    itemForm.controls['isHidden'].patchValue(false);
    item.isHidden = false;
    this.setItemDisplay$.next(item);
    this.notify.success('row shown');
  }

  hideRow(itemForm: FormGroup, item: models.SosItem) {
    itemForm.controls['isHidden'].patchValue(true);
    item.isHidden = true;
    this.setItemDisplay$.next(item);
    this.notify.success('row hidden');
  }

  cloneSupplyRow(item: models.SosItem) {
    if (this.isGridDirty())
      this.notify.warning('You must save or refresh before cloning a supply');
    else if (item.pipeContractId === null)
      this.notify.warning('You must select a pipe contract before cloning a supply');
    else
      this.cloneSupply$.next(item);
  }

  resetSupplyRow() {
    if (this.isGridDirty())
      this.notify.warning('You must save or refresh before resetting a supply');
    else
      this.supplyResetOpened$.next(true);
  }

  changeTransferDates() {
    if (this.isGridDirty())
      this.notify.warning('You must save or refresh before changing transfer dates');
    else
      this.changeTransferDatesOpened$.next(true);
  }

  toggleKeepWhole(nomForm: FormGroup) {
    const nomValue: models.SosNom = nomForm.value;
    const newIsKeepWhole: boolean = !nomValue.isKeepWhole;
    nomForm.controls['isKeepWhole'].patchValue(newIsKeepWhole);
    nomForm.controls['isKeepWhole'].markAsDirty();
  }

  setSelectedItem(itemForm: FormGroup) {
    //we only set lastSelectedItem if the itemForm has a value
    //we don't want lastSelectedItem to get set to null since it is passed into other components which might clear selection after opening
    if (itemForm?.value)
      this.lastSelectedItem = itemForm.value;
    const itemForms = this.itemForms as FormGroup[];
    SosHelper.setSelectedItem(itemForms, itemForm);
  }

  toggleShowHiddenRows() {
    this.gridLoading$.next(true);
    setTimeout(() => {
      this.mainShowHiddenRows = !this.mainShowHiddenRows;
      this.gridLoading$.next(false);
    }, 500);
  }

  showNote() {
    this.noteOpened$.next(true);
  }

  closeNote() {
    this.noteOpened$.next(false);
  }

  noteChanged(note: string) {
    const newNomNote: string = !util.isNullOrWhitespace(note) ? note.trim() : '';
    this.selectedNomForm.controls['notes'].patchValue(newNomNote);
    this.selectedNomForm.controls['notes'].markAsDirty();
    this.closeNote();
  }

  syncScroll() {
    if (this.frozenColValuesDiv && this.normalColValuesDiv)
      this.frozenColValuesDiv.nativeElement.scrollTop = this.normalColValuesDiv.nativeElement.scrollTop;
    if (this.normalColsTopSectionDiv && this.normalColValuesDiv)
      this.normalColsTopSectionDiv.nativeElement.scrollLeft = this.normalColValuesDiv.nativeElement.scrollLeft;
  }

  columnSort(event: Event, sortPropName: string) {
    const isOuterHeader = event.currentTarget === event.target;
    const isHeaderName = (event.target instanceof HTMLSpanElement) && event.target.classList.contains('supplyHeaderName');
    if (isOuterHeader || isHeaderName)
      this.sortChanged$.next(sortPropName);
  }

  removeHiddenSorts() {
    //remove hidden items from mainGridSort
    this.mainGridSort = this.mainGridSort.filter(gridSort => {
      const display = this.mainDisplayInfos.find(display => display.propName === gridSort.field);
      const isVisible = display && display.isVisible;
      return isVisible;
    });
  }

  updateDisplays() {
    //reset all sortNum and sortDir values
    this.mainDisplayInfos.forEach(x => {
      x.sortNum = null;
      x.sortDir = null;
    });

    let sortNum: number = 1;
    this.mainGridSort.forEach(x => {
      const displayInfo = this.mainDisplayInfos.find(y => y.propName === x.field);
      if (displayInfo) {
        displayInfo.sortDir = x.dir;
        displayInfo.sortNum = this.mainGridSort.length > 1 ? sortNum++ : null;
      }
      else
        displayInfo.sortDir = null;
    });

    this.displays = this.mainDisplayInfos.reduce((acc: { [key: string]: DisplayInfo }, x) => {
      acc[x.propName] = x;
      return acc;
    }, {});
  }

  clearSort() {
    this.sortChanged$.next('clear');
  }

  getSortedItems(dataSet: models.SosDataSet) {
    const newSort = this.mainGridSort.length > 0 ? this.mainGridSort : this.defaultSort;

    const pipeRecords: Record<number, string> = {};
    dataSet.pipeContracts.forEach(x => pipeRecords[x.contractId] = x.contractName);
    const propNames = ['pipeContractId', 'ptrContractId'];
    const pipeContractReplacer = { records: pipeRecords, propNames: propNames };

    const sortedItems = util.sortByProperties(dataSet.items, newSort, pipeContractReplacer);
    return sortedItems;
  }

  toggleLegend(): void {
    const isOpened = this.legendOpened$.value;
    this.legendOpened$.next(!isOpened);
  }

  closeLegend(): void {
    this.legendOpened$.next(false);
  }

  setClientOnlyProps(dataSet: models.SosDataSet) {
    dataSet.items.forEach(x => {
      x.isSelected = false;
      x.isHovered = false;
      x.isDynamicValueLoading = false;
    });
  }
}
