import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  DestroyRef,
  ElementRef,
  PLATFORM_ID,
  TemplateRef,
  computed,
  inject,
  input,
  signal,
  viewChild,
} from '@angular/core';
import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';

export interface CarouselItemContext<T> {
  $implicit: T;
  index: number;
}

export type CarouselTrackBy<T> = (index: number, item: T) => unknown;

interface CarouselPageItem<T> {
  item: T;
  index: number;
}

interface CarouselPage<T> {
  slot: 'previous' | 'current' | 'next';
  items: CarouselPageItem<T>[];
}

@Component({
  selector: 'app-carousel',
  imports: [NgTemplateOutlet],
  templateUrl: './carousel.component.html',
  styleUrl: './carousel.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CarouselComponent<T> implements AfterViewInit {
  private static readonly CONTROL_SLOT_WIDTH = 48;

  private readonly destroyRef = inject(DestroyRef);
  private readonly platformId = inject(PLATFORM_ID);
  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
  private readonly viewport = viewChild.required<ElementRef<HTMLElement>>('viewport');

  readonly items = input.required<readonly T[]>();
  readonly itemTemplate = input.required<TemplateRef<CarouselItemContext<T>>>();
  readonly trackBy = input<CarouselTrackBy<T>>((index) => index);
  readonly scrollStep = input<number | null>(null);
  readonly gap = input(0);
  readonly circular = input(false);
  readonly ariaLabel = input('Carrusel');
  readonly previousLabel = input('Mostrar elementos anteriores');
  readonly nextLabel = input('Mostrar elementos siguientes');

  protected readonly canScrollPrevious = signal(false);
  protected readonly canScrollNext = signal(false);
  protected readonly hasOverflow = signal(false);
  protected readonly itemWidth = signal<number | null>(null);
  protected readonly itemsPerPage = signal(Number.MAX_SAFE_INTEGER);
  protected readonly currentPage = signal(0);
  protected readonly transitionDirection = signal<-1 | 0 | 1>(0);
  protected readonly hasMultipleItems = computed(() => this.items().length > 1);
  protected readonly resolvedGap = computed(() => Math.max(this.gap(), 0));
  protected readonly pageCount = computed(() =>
    Math.max(1, Math.ceil(this.items().length / this.itemsPerPage())),
  );
  protected readonly isCircularLayout = computed(() => this.circular() && this.pageCount() > 1);
  protected readonly circularPages = computed<CarouselPage<T>[]>(() => {
    const pageCount = this.pageCount();
    const currentPage = this.normalizePage(this.currentPage(), pageCount);

    return [
      this.createPage('previous', currentPage - 1),
      this.createPage('current', currentPage),
      this.createPage('next', currentPage + 1),
    ];
  });
  protected readonly isPreviousDisabled = computed(
    () => !this.hasOverflow() || (!this.circular() && !this.canScrollPrevious()),
  );
  protected readonly isNextDisabled = computed(
    () => !this.hasOverflow() || (!this.circular() && !this.canScrollNext()),
  );

  ngAfterViewInit(): void {
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    const viewport = this.viewport().nativeElement;
    this.refreshLayout();

    const resizeObserver =
      typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(() => this.refreshLayout());
    resizeObserver?.observe(this.host.nativeElement);

    const mutationObserver =
      typeof MutationObserver === 'undefined'
        ? null
        : new MutationObserver(() => this.refreshLayout());
    mutationObserver?.observe(viewport, { childList: true, subtree: true });

    this.destroyRef.onDestroy(() => {
      resizeObserver?.disconnect();
      mutationObserver?.disconnect();
    });
  }

  previous(): void {
    if (this.circular()) {
      this.moveCircular(-1);
      return;
    }

    this.scroll(-1);
  }

  next(): void {
    if (this.circular()) {
      this.moveCircular(1);
      return;
    }

    this.scroll(1);
  }

  scrollTo(index: number): void {
    if (this.circular()) {
      if (index < 0 || index >= this.items().length) {
        return;
      }

      this.currentPage.set(Math.floor(index / this.itemsPerPage()));
      this.transitionDirection.set(0);
      return;
    }

    const item = this.viewport()
      .nativeElement.querySelectorAll<HTMLElement>('.carousel__item')
      .item(index);

    item?.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'start',
    });
  }

  protected handleScroll(): void {
    if (this.circular()) {
      return;
    }

    this.refreshNavigation();
  }

  protected finishCircularTransition(event: TransitionEvent): void {
    if (event.target !== event.currentTarget || event.propertyName !== 'transform') {
      return;
    }

    const direction = this.transitionDirection();

    if (direction === 0) {
      return;
    }

    this.currentPage.update((page) => this.normalizePage(page + direction, this.pageCount()));
    this.transitionDirection.set(0);
  }

  protected handleKeydown(event: KeyboardEvent): void {
    if (event.key === 'ArrowLeft') {
      event.preventDefault();
      this.previous();
    }

    if (event.key === 'ArrowRight') {
      event.preventDefault();
      this.next();
    }
  }

  private scroll(direction: -1 | 1): void {
    const viewport = this.viewport().nativeElement;
    const configuredStep = this.scrollStep();
    const step =
      configuredStep !== null && configuredStep > 0
        ? configuredStep
        : viewport.clientWidth + this.resolvedGap();

    viewport.scrollBy({
      left: direction * step,
      behavior: 'smooth',
    });
  }

  private moveCircular(direction: -1 | 1): void {
    if (!this.hasOverflow() || this.transitionDirection() !== 0) {
      return;
    }

    this.transitionDirection.set(direction);
  }

  private refreshNavigation(): void {
    const viewport = this.viewport().nativeElement;
    const maxScrollLeft = Math.max(viewport.scrollWidth - viewport.clientWidth, 0);
    const tolerance = 1;

    this.hasOverflow.set(maxScrollLeft > tolerance);
    this.canScrollPrevious.set(viewport.scrollLeft > tolerance);
    this.canScrollNext.set(viewport.scrollLeft < maxScrollLeft - tolerance);
  }

  private refreshLayout(): void {
    const viewport = this.viewport().nativeElement;
    const firstItem = viewport.querySelector<HTMLElement>('.carousel__item');
    const availableWidth = this.host.nativeElement.clientWidth;
    const availableViewportWidth = Math.max(
      availableWidth - (this.hasMultipleItems() ? CarouselComponent.CONTROL_SLOT_WIDTH * 2 : 0),
      0,
    );

    if (!firstItem || availableViewportWidth <= 0) {
      this.itemWidth.set(null);
      this.refreshNavigation();
      return;
    }

    const assignedWidth = firstItem.style.width;
    firstItem.style.width = '';
    const naturalItemWidth = firstItem.getBoundingClientRect().width;
    firstItem.style.width = assignedWidth;

    if (naturalItemWidth <= 0) {
      this.itemWidth.set(null);
      this.refreshNavigation();
      return;
    }

    const gap = this.resolvedGap();
    const fittingItems = Math.max(
      1,
      Math.floor((availableViewportWidth + gap) / (naturalItemWidth + gap)),
    );
    const visibleItems = Math.min(fittingItems, this.items().length);
    const distributedItemWidth =
      (availableViewportWidth - Math.max(visibleItems - 1, 0) * gap) / visibleItems;

    if (this.itemsPerPage() !== fittingItems) {
      this.itemsPerPage.set(fittingItems);
      this.currentPage.set(0);
      this.transitionDirection.set(0);
    }

    this.itemWidth.set(distributedItemWidth);
    if (this.circular()) {
      const hasOverflow = this.items().length > fittingItems;
      this.hasOverflow.set(hasOverflow);
      this.canScrollPrevious.set(hasOverflow);
      this.canScrollNext.set(hasOverflow);
    } else {
      this.refreshNavigation();
    }
  }

  private createPage(slot: CarouselPage<T>['slot'], page: number): CarouselPage<T> {
    const pageCount = this.pageCount();
    const normalizedPage = this.normalizePage(page, pageCount);
    const start = normalizedPage * this.itemsPerPage();
    const items: CarouselPageItem<T>[] = [];

    for (let offset = 0; offset < this.itemsPerPage(); offset += 1) {
      const index = (start + offset) % this.items().length;
      items.push({ item: this.items()[index], index });
    }

    return { slot, items };
  }

  private normalizePage(page: number, pageCount: number): number {
    return ((page % pageCount) + pageCount) % pageCount;
  }
}
