import { Injectable } from '@angular/core';
import { FlightsMapApiService } from '@app/features/flights-map/services/flights-map-api.service';
import { BehaviorSubject, Observable } from 'rxjs';

export interface IFlightsMapFilterState {
  departure?: string;
  arrival?: string;
  date      : Date;
  connections: boolean;
  domestic: boolean;
  international: boolean;

  disabledDates: Date[];
  minDate: Date;
  maxDate: Date;
}

@Injectable({
  providedIn: 'root'
})
export class FlightsMapFiltersStateService{

  private minDateCalendar: Date;
  private maxDateCalendar: Date;
  private disabledDatesDefault: Date[] = [];

  private disabledSet = new Set<number>();

  private _state$ = new BehaviorSubject<IFlightsMapFilterState>({
    date: null,
    connections: false,
    domestic: false,
    international: false,

    disabledDates: [],
    minDate: null,
    maxDate: null
  });

  readonly state$: Observable<IFlightsMapFilterState> = this._state$.asObservable();

  constructor(private apiService: FlightsMapApiService){
    const now = new Date();

    this.minDateCalendar = new Date(now); 
    this.minDateCalendar.setDate(now.getDate() - 1);

    this.maxDateCalendar = new Date(now);
    this.maxDateCalendar.setMonth(now.getMonth() + 6);

    const currentDate = new Date(now);
    currentDate.setDate(now.getDate() - 1);
    
    while(currentDate <= this.maxDateCalendar)
    {
      this.disabledDatesDefault.push(new Date(currentDate));
      currentDate.setDate(currentDate.getDate() + 1);
    }

    this.patch({
      date: null,
      connections: false,
      domestic: false,
      international: false,

      disabledDates: this.disabledDatesDefault,
      minDate: this.minDateCalendar,
      maxDate: this.maxDateCalendar
    })
  }

  setDeparture(code?: string) 
  { 
    if(!code)
    {
      this.patch({ departure: code, domestic: false, international: false, connections: false}); 
      this.setDisabledDates(this.disabledDatesDefault);
    }else
    {
      this.patch({ departure: code});
    }

    this.updateCalendarDates();
  }

  setArrival(code?: string) 
  { 
    this.patch({ arrival: code}); 

    this.updateCalendarDates();
  }

  setDate(date: Date)  
  { 
    if(date)
    {
      date.setHours(0, 0, 0, 0);
      this.patch({ date: date}); 
    }
    else
    {

      this.patch({ date: null}); 
    }
  }

  setDisabledDates(disabledDates: Date[]) {
    if(disabledDates)
    {
      this.patch({disabledDates: disabledDates});
    }
    else
    {
      this.patch({disabledDates: this.disabledDatesDefault});
    }

    this.disabledSet = new Set((disabledDates ?? []).map(d => this.toDateOnly(d).getTime()));
    this.snapToNearestAvailable();
    
  }

  setConnections(showConenctions: boolean)
  {
    this.patch({connections : showConenctions});
    this.updateCalendarDates();
  }

  setDomestic(showDomestic: boolean)
  {
    if(showDomestic)
    {
      this.patch({domestic: true, international: false})
    }
    else
    {
      this.patch({domestic: false})
    }
  }

  setInternational(showInternational: boolean)
  {
    if(showInternational)
    {
      this.patch({domestic: false, international: true})
    }
    else
    {
      this.patch({international: false})
    }
  }

  private patch(part: Partial<IFlightsMapFilterState>) 
  {
    this._state$.next(
      { 
        ...this._state$.value, 
        ...part
      }
    );
  }

  private updateCalendarDates() {
    if(this._state$.value.departure == this._state$.value.arrival){
      return;
    }

    this.apiService.getFlightDaysByRoute(this.minDateCalendar, this._state$.value.departure, this._state$.value.arrival, this._state$.value.connections)
      .subscribe((res) => {

        if (res != null) {
          const disabledDates = new Array<Date>();

          let date = new Date(this.minDateCalendar);

          for (let i = 0; i < res.days.length; i++) {
            if (res.days[i] == '0') {
              disabledDates.push(new Date(date));
            }
            date.setDate(date.getDate() + 1);
          }

          this.setDisabledDates(disabledDates);
        }
      });
  }

  private snapToNearestAvailable(){
    const anchor = this.clampToRange(this.toDateOnly(new Date()));

    if (this.isEnabled(anchor)){
      this.setIfChanged(anchor);
      return;
    }
   
    const start = this._state$.value.minDate ? this.toDateOnly(this._state$.value.minDate) : new Date(2024, 0, 1);
    const end   = this._state$.value.maxDate ? this.toDateOnly(this._state$.value.maxDate) : new Date(2040, 0, 1);

    const totalSpan = this.diffInDays(end, start);
    for (let delta = 1; delta <= totalSpan; delta++) 
    {
      const forward = this.addDays(anchor, delta);
      if (forward <= end && this.isEnabled(forward)) 
      { 
          this.setIfChanged(forward); 
          return; 
      }
    }

    this.setIfChanged(null);
  }

  private setIfChanged(newDate?: Date) {
    const curr = this._state$.value.date ? this.toDateOnly(this._state$.value.date).getTime() : null;
    const next = newDate ? this.toDateOnly(newDate).getTime() : null;
    if (curr !== next)
    {
      this.patch({ date: newDate ?? null });
    }
  }

  private isEnabled(d: Date): boolean {
    const t = this.toDateOnly(d).getTime();

    const minDate = this._state$.value.minDate;
    const maxDate = this._state$.value.maxDate;
    if (this.disabledSet.has(t)) return false;
    if (minDate && t < this.toDateOnly(minDate).getTime()) return false;
    if (maxDate && t > this.toDateOnly(maxDate).getTime()) return false;

    return true;
  }

  private toDateOnly(d: Date): Date {
    const x = new Date(d);
    x.setHours(0, 0, 0, 0);
    return x;
  }

  private clampToRange(d: Date): Date {
    const x = this.toDateOnly(d);

    const minDate = this._state$.value.minDate;
    const maxDate = this._state$.value.maxDate;

    if (minDate && x < this.toDateOnly(minDate)) return this.toDateOnly(minDate);
    if (maxDate && x > this.toDateOnly(maxDate)) return this.toDateOnly(maxDate);
    return x;
  }

  private diffInDays(a: Date, b: Date): number {
    return Math.abs(Math.floor((this.toDateOnly(a).getTime() - this.toDateOnly(b).getTime()) / 86400000));
    // 86_400_000 мс в сутках
  }

  private addDays(d: Date, days: number): Date {
    const x = new Date(d);
    x.setDate(x.getDate() + days);
    x.setHours(0, 0, 0, 0);
    return x;
  }
}
