import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
import naturalCompare from "string-natural-compare";
import { Chip } from "./Chip";
import { PageSelector } from "./Inputs/PageSelector";
import { Ico, Icon } from "./Ico";
import { cJoin } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { useIs3ColumnsLayout } from "hooks/useContainerQuery";
import { useAppLayout } from "contexts/AppLayoutContext";
import { Ids } from "types/ids";

interface Group<T> {
  name: string;
  items: T[];
  totalCount: number;
}

const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
  naturalCompare(a.name, b.name);
const defaultGroupCountingFunction = () => 1;
const defaultFilteringFunction = () => true;
const defaultSortingFunction = () => 0;
const defaultGroupingFunction = () => [""];

interface Props<T> {
  // Items
  items: T[];
  getItemId: (item: T) => string;
  renderItem: (props: { item: T }) => JSX.Element;
  renderWhenEmpty?: () => JSX.Element;
  // Pagination
  paginationItemPerPage?: number;
  paginationSelectorTop?: boolean;
  paginationSelectorBottom?: boolean;
  paginationScroolTop?: boolean;
  // Searching
  searchingTerm?: string;
  searchingBy?: (item: T) => string;
  searchingCaseInsensitive?: boolean;
  // Grouping
  groupingFunction?: (item: T) => string[];
  groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
  groupCountingFunction?: (item: T) => number;
  // Filtering
  filteringFunction?: (item: T) => boolean;
  // Sorting
  sortingFunction?: (a: T, b: T) => number;
  // Other
  className?: string;
}

export const SmartList = <T,>({
  items,
  getItemId,
  renderItem: RenderItem,
  renderWhenEmpty: RenderWhenEmpty,
  paginationItemPerPage = Infinity,
  paginationSelectorTop = true,
  paginationSelectorBottom = true,
  paginationScroolTop = true,
  searchingTerm,
  searchingBy,
  searchingCaseInsensitive = true,
  groupingFunction = defaultGroupingFunction,
  groupSortingFunction = defaultGroupSortingFunction,
  groupCountingFunction = defaultGroupCountingFunction,
  filteringFunction = defaultFilteringFunction,
  sortingFunction = defaultSortingFunction,
  className,
}: Props<T>): JSX.Element => {
  const [page, setPage] = useState(0);
  const { langui } = useAppLayout();
  useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
  useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction, items]);

  const searchFilter = useCallback(() => {
    if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
      if (searchingCaseInsensitive) {
        return items.filter((item) =>
          searchingBy(item).toLowerCase().includes(searchingTerm.toLowerCase())
        );
      }
      return items.filter((item) => searchingBy(item).includes(searchingTerm));
    }
    return items;
  }, [items, searchingBy, searchingCaseInsensitive, searchingTerm]);

  const filteredItems = useMemo(() => {
    const filteredBySearch = searchFilter();
    return filteredBySearch.filter(filteringFunction);
  }, [filteringFunction, searchFilter]);

  const sortedItem = useMemo(
    () => filteredItems.sort(sortingFunction),
    [filteredItems, sortingFunction]
  );

  const groups = useMemo(() => {
    const memo: Group<T>[] = [];

    sortedItem.forEach((item) => {
      groupingFunction(item).forEach((category) => {
        const index = memo.findIndex((group) => group.name === category);
        if (index === -1) {
          memo.push({
            name: category,
            items: [item],
            totalCount: groupCountingFunction(item),
          });
        } else {
          memo[index].items.push(item);
          memo[index].totalCount += groupCountingFunction(item);
        }
      });
    });
    return memo.sort(groupSortingFunction);
  }, [groupCountingFunction, groupSortingFunction, groupingFunction, sortedItem]);

  const pages = useMemo(() => {
    const memo: Group<T>[][] = [];
    let currentPage: Group<T>[] = [];
    let remainingSlots = paginationItemPerPage;
    let loopSafeguard = 1000;

    const newPage = () => {
      memo.push(currentPage);
      currentPage = [];
      remainingSlots = paginationItemPerPage;
    };

    for (const group of groups) {
      let remainingItems = group.items.length;
      while (remainingItems > 0 && loopSafeguard >= 0) {
        loopSafeguard--;
        const currentIndex = group.items.length - remainingItems;

        if (
          remainingSlots <= 0 ||
          (currentIndex === 0 &&
            remainingItems > remainingSlots &&
            remainingItems <= paginationItemPerPage)
        ) {
          newPage();
        }

        const slicedGroup: Group<T> = {
          name: group.name,
          items: group.items.slice(currentIndex, currentIndex + remainingSlots),
          totalCount: group.totalCount,
        };

        remainingItems -= slicedGroup.items.length;
        remainingSlots -= slicedGroup.items.length;
        currentPage.push(slicedGroup);
      }
    }

    if (currentPage.length > 0) {
      newPage();
    }

    return memo;
  }, [groups, paginationItemPerPage]);

  return (
    <>
      {pages.length > 1 && paginationSelectorTop && (
        <PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
      )}

      <div className="mb-8">
        {pages[page]?.length > 0 ? (
          pages[page]?.map(
            (group) =>
              group.items.length > 0 && (
                <Fragment key={group.name}>
                  {group.name.length > 0 && (
                    <h2
                      className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
                      first-of-type:pt-0">
                      {group.name}
                      <Chip
                        text={`${group.totalCount} ${
                          group.items.length <= 1
                            ? langui.result?.toLowerCase() ?? ""
                            : langui.results?.toLowerCase() ?? ""
                        }`}
                      />
                    </h2>
                  )}
                  <div
                    className={cJoin(
                      `grid items-start gap-8 border-b-[3px] border-dotted pb-12
                      last-of-type:border-0`,
                      className
                    )}>
                    {group.items.map((item) => (
                      <RenderItem item={item} key={getItemId(item)} />
                    ))}
                  </div>
                </Fragment>
              )
          )
        ) : isDefined(RenderWhenEmpty) ? (
          <RenderWhenEmpty />
        ) : (
          <DefaultRenderWhenEmpty />
        )}
      </div>

      {pages.length > 1 && paginationSelectorBottom && (
        <PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
      )}
    </>
  );
};

/*
 *                                    ╭──────────────────────╮
 * ───────────────────────────────────╯  PRIVATE COMPONENTS  ╰──────────────────────────────────────
 */

const DefaultRenderWhenEmpty = () => {
  const is3ColumnsLayout = useIs3ColumnsLayout();
  const { langui } = useAppLayout();
  return (
    <div className="grid h-full place-content-center">
      <div
        className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
        border-dark p-8 text-dark opacity-40">
        {is3ColumnsLayout && <Ico icon={Icon.ChevronLeft} className="!text-[300%]" />}
        <p className="max-w-xs text-2xl">{langui.no_results_message}</p>
        {!is3ColumnsLayout && <Ico icon={Icon.ChevronRight} className="!text-[300%]" />}
      </div>
    </div>
  );
};