import { inject, isSignal, Signal } from '@angular/core';
import { BreakpointObserver } from '@angular/cdk/layout';
import { exhaustMap, isObservable, map, Observable, of, startWith } from 'rxjs';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { coerceArray } from '@angular/cdk/coercion';

/**
 * Enum-like object defining various breakpoints for responsive design.
 */
export const Breakpoint = {
  Tablet: 'tablet',
  Desktop: 'desktop',
  DesktopL: 'desktop-l',
  DesktopXL: 'desktop-xl',
} as const;

/**
 * Type representing the possible values of the Breakpoint object.
 */
export type Breakpoint = (typeof Breakpoint)[keyof typeof Breakpoint];

/**
 * Mapping of Breakpoint values to their corresponding CSS media query selectors.
 */
const BreakpointSelector = {
  [Breakpoint.Tablet]: '(max-width: 1279px)',
  [Breakpoint.Desktop]: '(min-width: 1280px) and (max-width: 1439px)',
  [Breakpoint.DesktopL]: '(min-width: 1440px) and (max-width: 1919px)',
  [Breakpoint.DesktopXL]: '(min-width: 1920px)',
};

/**
 * Observes viewport breakpoints and returns an Observable that emits whether the specified breakpoints match.
 *
 * This function accepts a Breakpoint, an array of Breakpoints, a Signal, or an Observable thereof and returns
 * an Observable that emits a boolean indicating if the given breakpoints match the current viewport.
 *
 * @param breakpoints - A Breakpoint, array of Breakpoints, Signal, or Observable representing breakpoints.
 * @returns An Observable that emits a boolean indicating if the specified breakpoints match.
 */
export function breakpoint$(
  breakpoints:
    | (Breakpoint | Breakpoint[])
    | Signal<Breakpoint | Breakpoint[]>
    | Observable<Breakpoint | Breakpoint[]>,
): Observable<boolean> {
  const bpObserver = inject(BreakpointObserver);

  const breakpoints$ = isObservable(breakpoints)
    ? breakpoints
    : isSignal(breakpoints)
      ? toObservable(breakpoints)
      : of(breakpoints);

  const breakpointSelectors$ = breakpoints$.pipe(
    map((bp) => coerceArray(bp).map((b) => BreakpointSelector[b])),
  );

  const match$ = breakpointSelectors$.pipe(
    exhaustMap((selectors) =>
      bpObserver.observe(selectors).pipe(
        map((result) => result.matches),
        startWith(bpObserver.isMatched(selectors)),
      ),
    ),
  );
  return match$;
}

/**
 * Converts the breakpoint$ Observable into a Signal to provide reactive breakpoint matching.
 *
 * This function wraps the breakpoint$ function, converting its Observable result into a Signal,
 * which can be used with Angular's reactive system. The Signal returns a boolean indicating whether
 * the specified breakpoints currently match or undefined if not yet determined.
 *
 * @param breakpoints - A Breakpoint, array of Breakpoints, Signal, or Observable representing breakpoints.
 * @returns A Signal<boolean | undefined> indicating the match state of the breakpoints.
 */
export function breakpoint(
  breakpoints:
    | (Breakpoint | Breakpoint[])
    | Signal<Breakpoint | Breakpoint[]>
    | Observable<Breakpoint | Breakpoint[]>,
): Signal<boolean | undefined> {
  return toSignal(breakpoint$(breakpoints));
}
