import type { BooleanInput, NumberInput } from '@angular/cdk/coercion';
import {
  ChangeDetectionStrategy,
  Component,
  booleanAttribute,
  computed,
  input,
  model,
  numberAttribute,
  untracked,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HlmSelectImports } from '@spartan-ng/helm/select';
import { HlmPagination } from './hlm-pagination';
import { HlmPaginationContent } from './hlm-pagination-content';
import { HlmPaginationEllipsis } from './hlm-pagination-ellipsis';
import { HlmPaginationItem } from './hlm-pagination-item';
import { HlmPaginationLink } from './hlm-pagination-link';
import { HlmPaginationNext } from './hlm-pagination-next';
import { HlmPaginationPrevious } from './hlm-pagination-previous';

@Component({
  selector: 'hlm-numbered-pagination',
  imports: [
    FormsModule,
    HlmPagination,
    HlmPaginationContent,
    HlmPaginationItem,
    HlmPaginationPrevious,
    HlmPaginationNext,
    HlmPaginationLink,
    HlmPaginationEllipsis,
    HlmSelectImports,
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex items-center justify-between gap-2 px-4 py-2">
      <div class="flex items-center gap-1 text-sm text-nowrap text-gray-600">
        <b>{{ totalItems() }}</b>
        total items |
        <b>{{ _lastPageNumber() }}</b>
        pages
      </div>

      <nav hlmPagination>
        <ul hlmPaginationContent>
          @if (showEdges() && !_isFirstPageActive()) {
            <li
              hlmPaginationItem
              (click)="goToPrevious()"
            >
              <hlm-pagination-previous />
            </li>
          }

          @for (page of _pages(); track page) {
            <li hlmPaginationItem>
              @if (page === '...') {
                <hlm-pagination-ellipsis />
              } @else {
                <a
                  [isActive]="currentPage() === page"
                  hlmPaginationLink
                  (click)="currentPage.set(page)"
                >
                  {{ page }}
                </a>
              }
            </li>
          }

          @if (showEdges() && !_isLastPageActive()) {
            <li
              hlmPaginationItem
              (click)="goToNext()"
            >
              <hlm-pagination-next />
            </li>
          }
        </ul>
      </nav>

      <!-- Show Page Size selector -->
      <hlm-select
        class="ml-auto"
        [(ngModel)]="itemsPerPage"
      >
        <hlm-select-trigger class="w-fit">
          <hlm-select-value />
        </hlm-select-trigger>
        <hlm-select-content *hlmSelectPortal>
          <hlm-select-group>
            @for (pageSize of _pageSizesWithCurrent(); track pageSize) {
              <hlm-select-item [value]="pageSize">{{ pageSize }}</hlm-select-item>
            }
          </hlm-select-group>
        </hlm-select-content>
      </hlm-select>
    </div>
  `,
})
export class HlmNumberedPagination {
  /**
   * The current (active) page.
   */
  public readonly currentPage = model.required<number>();

  /**
   * The number of items per paginated page.
   */
  public readonly itemsPerPage = model.required<number>();

  /**
   * The total number of items in the collection. Only useful when
   * doing server-side paging, where the collection size is limited
   * to a single page returned by the server API.
   */
  public readonly totalItems = input.required<number, NumberInput>({
    transform: numberAttribute,
  });

  /**
   * The number of page links to show.
   */
  public readonly maxSize = input<number, NumberInput>(7, {
    transform: numberAttribute,
  });

  /**
   * Show the first and last page buttons.
   */
  public readonly showEdges = input<boolean, BooleanInput>(true, {
    transform: booleanAttribute,
  });

  /**
   * The page sizes to show.
   * Defaults to [10, 20, 50, 100]
   */
  public readonly pageSizes = input<number[]>([10, 20, 50, 100]);

  protected readonly _pageSizesWithCurrent = computed(() => {
    const pageSizes = this.pageSizes();
    return pageSizes.includes(this.itemsPerPage())
      ? pageSizes // if current page size is included, return the same array
      : [...pageSizes, this.itemsPerPage()].sort((a, b) => a - b); // otherwise, add current page size and sort the array
  });

  protected readonly _isFirstPageActive = computed(() => this.currentPage() === 1);
  protected readonly _isLastPageActive = computed(
    () => this.currentPage() === this._lastPageNumber(),
  );

  protected readonly _lastPageNumber = computed(() => {
    if (this.totalItems() < 1) {
      // when there are 0 or fewer (an error case) items, there are no "pages" as such,
      // but it makes sense to consider a single, empty page as the last page.
      return 1;
    }
    return Math.ceil(this.totalItems() / this.itemsPerPage());
  });

  protected readonly _pages = computed(() => {
    const correctedCurrentPage = outOfBoundCorrection(
      this.totalItems(),
      this.itemsPerPage(),
      this.currentPage(),
    );

    if (correctedCurrentPage !== this.currentPage()) {
      // update the current page
      untracked(() => this.currentPage.set(correctedCurrentPage));
    }

    return createPageArray(
      correctedCurrentPage,
      this.itemsPerPage(),
      this.totalItems(),
      this.maxSize(),
    );
  });

  protected goToPrevious(): void {
    this.currentPage.set(this.currentPage() - 1);
  }

  protected goToNext(): void {
    this.currentPage.set(this.currentPage() + 1);
  }

  protected goToFirst(): void {
    this.currentPage.set(1);
  }

  protected goToLast(): void {
    this.currentPage.set(this._lastPageNumber());
  }
}

type Page = number | '...';

/**
 * Checks that the instance.currentPage property is within bounds for the current page range.
 * If not, return a correct value for currentPage, or the current value if OK.
 *
 * Copied from 'ngx-pagination' package
 */
export function outOfBoundCorrection(
  totalItems: number,
  itemsPerPage: number,
  currentPage: number,
): number {
  const totalPages = Math.ceil(totalItems / itemsPerPage);
  if (totalPages < currentPage && 0 < totalPages) {
    return totalPages;
  }

  if (currentPage < 1) {
    return 1;
  }

  return currentPage;
}

/**
 * Returns an array of Page objects to use in the pagination controls.
 *
 * Copied from 'ngx-pagination' package
 */
export function createPageArray(
  currentPage: number,
  itemsPerPage: number,
  totalItems: number,
  paginationRange: number,
): Page[] {
  // paginationRange could be a string if passed from attribute, so cast to number.
  paginationRange = +paginationRange;
  const pages: Page[] = [];

  // Return 1 as default page number
  // Make sense to show 1 instead of empty when there are no items
  const totalPages = Math.max(Math.ceil(totalItems / itemsPerPage), 1);
  const halfWay = Math.ceil(paginationRange / 2);

  const isStart = currentPage <= halfWay;
  const isEnd = totalPages - halfWay < currentPage;
  const isMiddle = !isStart && !isEnd;

  const ellipsesNeeded = paginationRange < totalPages;
  let i = 1;

  while (i <= totalPages && i <= paginationRange) {
    let label: number | '...';
    const pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
    const openingEllipsesNeeded = i === 2 && (isMiddle || isEnd);
    const closingEllipsesNeeded = i === paginationRange - 1 && (isMiddle || isStart);
    if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
      label = '...';
    } else {
      label = pageNumber;
    }
    pages.push(label);
    i++;
  }

  return pages;
}

/**
 * Given the position in the sequence of pagination links [i],
 * figure out what page number corresponds to that position.
 *
 * Copied from 'ngx-pagination' package
 */
function calculatePageNumber(
  i: number,
  currentPage: number,
  paginationRange: number,
  totalPages: number,
) {
  const halfWay = Math.ceil(paginationRange / 2);
  if (i === paginationRange) {
    return totalPages;
  }

  if (i === 1) {
    return i;
  }

  if (paginationRange < totalPages) {
    if (totalPages - halfWay < currentPage) {
      return totalPages - paginationRange + i;
    }
    if (halfWay < currentPage) {
      return currentPage - halfWay + i;
    }
    return i;
  }

  return i;
}
