import {
  Component,
  createSignal,
  ErrorBoundary,
  type Ref,
  createSelector,
  Index,
  createMemo,
  For,
  createUniqueId,
} from "solid-js";
import { type mastodon } from "masto";
import { vibrate } from "~platform/hardware";
import { useDefaultSession } from "../masto/clients";
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
import RegularToot, {
  findElementActionable,
  findRootToot,
  TootEnvProvider,
} from "./RegularToot";
import cardStyle from "~material/cards.module.css";
import type { ThreadNode } from "../masto/timelines";
import { useNavigator } from "~platform/StackedRouter";
import { ANIM_CURVE_STD } from "~material/theme";
import { useItemSelection } from "./toots/ItemSelectionProvider";

function durationOf(rect0: DOMRect, rect1: DOMRect) {
  const distancelt = Math.sqrt(
    Math.pow(Math.abs(rect0.top - rect1.top), 2) +
      Math.pow(Math.abs(rect0.left - rect1.left), 2),
  );
  const distancerb = Math.sqrt(
    Math.pow(Math.abs(rect0.bottom - rect1.bottom), 2) +
      Math.pow(Math.abs(rect0.right - rect1.right), 2),
  );
  const distance = distancelt + distancerb;
  const duration = distance / 1.6;
  return duration;
}

function positionTootInThread(index: number, threadLength: number) {
  if (index === 0) {
    return "top";
  } else if (index === threadLength - 1) {
    return "bottom";
  }
  return "middle";
}

/**
 * Full-feature toot list.
 */
const TootList: Component<{
  ref?: Ref<HTMLDivElement>;
  id?: string;
  threads: readonly string[];
  onUnknownThread: (id: string) => ThreadNode[] | undefined;
  onChangeToot: (id: string, value: mastodon.v1.Status) => void;
}> = (props) => {
  const session = useDefaultSession();
  const [isExpanded, setExpanded] = useItemSelection();
  const { push } = useNavigator();

  const onBookmark = async (status: mastodon.v1.Status) => {
    const client = session()?.client;
    if (!client) return;

    const result = await (status.bookmarked
      ? client.v1.statuses.$select(status.id).unbookmark()
      : client.v1.statuses.$select(status.id).bookmark());
    props.onChangeToot(result.id, result);
  };

  const toggleBoost = async (status: mastodon.v1.Status) => {
    const client = session()?.client;
    if (!client) return;

    vibrate(50);
    const rootStatus = status.reblog ? status.reblog : status;
    const reblogged = rootStatus.reblogged;
    if (status.reblog) {
      props.onChangeToot(status.id, {
        ...status,
        reblog: { ...status.reblog, reblogged: !reblogged },
      });
    } else {
      props.onChangeToot(status.id, {
        ...status,
        reblogged: !reblogged,
      });
    }

    const result = reblogged
      ? await client.v1.statuses.$select(status.id).unreblog()
      : (await client.v1.statuses.$select(status.id).reblog()).reblog!;

    if (status.reblog) {
      props.onChangeToot(status.id, {
        ...status,
        reblog: result,
      });
    } else {
      props.onChangeToot(status.id, result);
    }
  };

  const toggleFavourite = async (status: mastodon.v1.Status) => {
    const client = session()?.client;
    if (!client) return;
    const ovalue = status.favourited;
    props.onChangeToot(status.id, { ...status, favourited: !ovalue });

    const result = ovalue
      ? await client.v1.statuses.$select(status.id).unfavourite()
      : await client.v1.statuses.$select(status.id).favourite();
    props.onChangeToot(status.id, result);
  };

  const openFullScreenToot = (
    toot: mastodon.v1.Status,
    srcElement: HTMLElement,
    reply?: boolean,
  ) => {
    const p = session()?.account;
    if (!p) return;
    const inf = p.inf;
    if (!inf) {
      console.warn("no account info?");
      return;
    }

    const acct = `${inf.username}@${p.site}`;
    setTootBottomSheetCache(acct, toot);

    push(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
      animateOpen(element) {
        const rect0 = srcElement.getBoundingClientRect(); // the start rect
        const rect1 = element.getBoundingClientRect(); // the end rect

        const duration = durationOf(rect0, rect1);

        const keyframes = {
          top: [`${rect0.top}px`, `${rect1.top}px`],
          bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
          left: [`${rect0.left}px`, `${rect1.left}px`],
          right: [`${rect0.right}px`, `${rect1.right}px`],
          height: [`${rect0.height}px`, `${rect1.height}px`],
          margin: 0,
        };

        srcElement.style.visibility = "hidden";

        const animation = element.animate(keyframes, {
          duration,
          easing: ANIM_CURVE_STD,
        });
        return animation;
      },

      animateClose(element) {
        const rect0 = element.getBoundingClientRect(); // the start rect
        const rect1 = srcElement.getBoundingClientRect(); // the end rect

        const duration = durationOf(rect0, rect1);

        const keyframes = {
          top: [`${rect0.top}px`, `${rect1.top}px`],
          bottom: [`${rect0.bottom}px`, `${rect1.bottom}px`],
          left: [`${rect0.left}px`, `${rect1.left}px`],
          right: [`${rect0.right}px`, `${rect1.right}px`],
          height: [`${rect0.height}px`, `${rect1.height}px`],
          margin: 0,
        };

        srcElement.style.visibility = "";

        const animation = element.animate(keyframes, {
          duration,
          easing: ANIM_CURVE_STD,
        });
        return animation;
      },
    });
  };

  const onItemClick = (
    status: mastodon.v1.Status,
    event: MouseEvent & { target: EventTarget; currentTarget: HTMLElement },
  ) => {
    if (!(event.target instanceof HTMLElement)) {
      throw new Error("target is not an element");
    }
    const actionableElement = findElementActionable(
      event.target,
      event.currentTarget,
    );

    if (actionableElement && isExpanded(event.currentTarget.id)) {
      if (actionableElement.dataset.action === "acct") {
        event.stopPropagation();

        const target = actionableElement as HTMLAnchorElement;

        const acct = encodeURIComponent(
          target.dataset.client || `@${new URL(target.href).origin}`,
        );

        push(`/${acct}/profile/${target.dataset.acctId}`);

        return;
      } else {
        console.warn("unknown action", actionableElement.dataset.rel);
      }
    } else if (
      event.target.parentElement &&
      event.target.parentElement.tagName === "A"
    ) {
      return;
    }

    // else if (!actionableElement || !checkIsExpended(status) || <rel is not one of known action>)
    if (!isExpanded(event.currentTarget.id)) {
      setExpanded(event.currentTarget.id);
    } else {
      openFullScreenToot(status, event.currentTarget as HTMLElement);
    }
  };

  const reply = (
    status: mastodon.v1.Status,
    event: { currentTarget: HTMLElement },
  ) => {
    const element = findRootToot(event.currentTarget);
    openFullScreenToot(status, element, true);
  };

  const vote = async (status: mastodon.v1.Status, votes: readonly number[]) => {
    const client = session()?.client;
    if (!client) return;

    const toot = status.reblog ?? status;
    if (!toot.poll) return;

    const npoll = await client.v1.polls.$select(toot.poll.id).votes.create({
      choices: votes,
    });

    if (status.reblog) {
      props.onChangeToot(status.id, {
        ...status,
        reblog: {
          ...status.reblog,
          poll: npoll,
        },
      });
    } else {
      props.onChangeToot(status.id, {
        ...status,
        poll: npoll,
      });
    }
  };

  return (
    <ErrorBoundary
      fallback={(err, reset) => {
        console.error(err);
        return <p>Oops: {String(err)}</p>;
      }}
    >
      <TootEnvProvider
        value={{
          boost: toggleBoost,
          bookmark: onBookmark,
          favourite: toggleFavourite,
          reply: reply,
          vote: vote,
        }}
      >
        <div ref={props.ref} id={props.id} class="toot-list">
          <For each={props.threads}>
            {(threadId, threadIdx) => {
              const thread = createMemo(() =>
                props.onUnknownThread(threadId)?.reverse(),
              );

              const threadLength = () => thread()?.length ?? 0;

              return (
                <Index each={thread()}>
                  {(threadNode, index) => {
                    const status = () => threadNode().value;
                    const id = createUniqueId();

                    return (
                      <RegularToot
                        data-status-id={status().id}
                        data-thread-sort={index}
                        id={id}
                        status={status()}
                        thread={
                          threadLength() > 1
                            ? positionTootInThread(index, threadLength())
                            : undefined
                        }
                        class={cardStyle.card}
                        evaluated={isExpanded(id)}
                        actionable={isExpanded(id)}
                        onClick={[onItemClick, status()]}
                      />
                    );
                  }}
                </Index>
              );
            }}
          </For>
        </div>
      </TootEnvProvider>
    </ErrorBoundary>
  );
};

export default TootList;
