import { NgTemplateOutlet } from '@angular/common';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ContentChild,
  ElementRef,
  TemplateRef,
  ViewChild,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked
} from '@angular/core';
import { injectVirtualizer } from '@tanstack/angular-virtual';
import { VirtualRowMeasureDirective } from './virtual-row-measure.directive';

/**
 * Headless virtual scroller for vertical lists. Wraps `@tanstack/angular-virtual`
 * and exposes the visible window to a consumer-provided `<ng-template #item>`.
 *
 * Why this exists: large `@for` loops over hundreds of rows freeze the render
 * thread on Electron. Virtualization keeps the DOM bounded to the viewport so
 * pointer/scroll input stays responsive even with multi-thousand-item lists.
 *
 * Usage:
 * ```html
 * <app-virtual-list
 *   [items]="rows()"
 *   [estimateSize]="48"
 *   [trackBy]="trackById"
 *   class="block h-full"
 * >
 *   <ng-template #item let-row let-index="index">
 *     <my-row [row]="row" />
 *   </ng-template>
 * </app-virtual-list>
 * ```
 *
 * The host element is the scrollable viewport. Apply `class="block h-full"`
 * (or any fixed-height styling) on the host so the inner virtualizer can
 * measure a real scroll height. Variable row heights are measured
 * automatically via `ResizeObserver` (TanStack's `measureElement`).
 */
@Component({
  selector: 'app-virtual-list',
  standalone: true,
  imports: [NgTemplateOutlet, VirtualRowMeasureDirective],
  templateUrl: './virtual-list.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'tk-virtual-list block overflow-y-auto',
    style: 'contain: strict;'
  }
})
export class VirtualListComponent<T> implements AfterViewInit {
  @ContentChild('item', { static: true })
  protected itemTemplate?: TemplateRef<{ $implicit: T; index: number }>;

  @ViewChild('measure', { static: true })
  protected readonly measureRef!: ElementRef<HTMLDivElement>;

  readonly items = input.required<readonly T[]>();
  readonly estimateSize = input<number>(48);
  readonly overscan = input<number>(6);
  readonly trackBy = input<(index: number, item: T) => string | number>();
  readonly paddingStart = input<number>(0);
  readonly paddingEnd = input<number>(0);
  readonly horizontal = input<boolean>(false);
  readonly initialOffset = input<number | undefined>(undefined);

  protected readonly virtualizer = injectVirtualizer<HTMLElement, HTMLElement>(() => ({
    count: this.items().length,
    scrollElement: this.host() ?? undefined,
    estimateSize: () => this.estimateSize(),
    overscan: this.overscan(),
    paddingStart: this.paddingStart(),
    paddingEnd: this.paddingEnd(),
    horizontal: this.horizontal(),
    initialOffset: this.initialOffset(),
    getItemKey: (index: number) => {
      const track = this.trackBy();
      const list = this.items();

      if (track && index < list.length)
        return track(index, list[index]);

      return index;
    }
  }));

  protected readonly virtualItems = this.virtualizer.getVirtualItems;
  protected readonly totalSize = this.virtualizer.getTotalSize;

  protected readonly translateY = computed(() => {
    const first = this.virtualItems()[0];

    return first ? first.start : 0;
  });

  private readonly hostRef = inject<ElementRef<HTMLElement>>(ElementRef);
  private readonly host = signal<HTMLElement | null>(null);

  // Force re-measurement when items change identity (e.g. data refresh).
  private readonly onItemsChanged = effect(() => {
    void this.items();
    untracked(() => {
      this.virtualizer().measure();
    });
  });

  ngAfterViewInit(): void {
    this.host.set(this.hostRef.nativeElement);
  }

  /** Imperative API: scroll to a given item index. */
  scrollToIndex(index: number, align: 'auto' | 'start' | 'center' | 'end' = 'auto'): void {
    this.virtualizer().scrollToIndex(index, { align });
  }

  /** Imperative API: scroll to a raw pixel offset. */
  scrollToOffset(offset: number, align: 'auto' | 'start' | 'center' | 'end' = 'auto'): void {
    this.virtualizer().scrollToOffset(offset, { align });
  }
}
