/*!
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
import { Box, Code, VStack, IconButton } from "@chakra-ui/react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { type JSX, useLayoutEffect, useRef, useCallback, useEffect } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useTranslation } from "react-i18next";
import { FiChevronDown, FiChevronUp } from "react-icons/fi";

import { ErrorAlert } from "src/components/ErrorAlert";
import { ProgressBar, Tooltip } from "src/components/ui";
import { getMetaKey } from "src/utils";

import { scrollToBottom, scrollToTop } from "./utils";

export type TaskLogContentProps = {
  readonly error: unknown;
  readonly isLoading: boolean;
  readonly logError: unknown;
  readonly parsedLogs: Array<JSX.Element | string | undefined>;
  readonly wrap: boolean;
};

// How close to the bottom (in px) before we consider the user "at the bottom"
const SCROLL_BOTTOM_THRESHOLD = 100;

const ScrollToButton = ({
  direction,
  onClick,
}: {
  readonly direction: "bottom" | "top";
  readonly onClick: () => void;
}) => {
  const { t: translate } = useTranslation("common");

  return (
    <Tooltip
      closeDelay={100}
      content={translate("scroll.tooltip", {
        direction: translate(`scroll.direction.${direction}`),
        hotkey: `${getMetaKey()}+${direction === "bottom" ? "↓" : "↑"}`,
      })}
      openDelay={100}
    >
      <IconButton
        _ltr={{
          left: "auto",
          right: 4,
        }}
        _rtl={{
          left: 4,
          right: "auto",
        }}
        aria-label={translate(`scroll.direction.${direction}`)}
        bg="bg.panel"
        bottom={direction === "bottom" ? 4 : 14}
        onClick={onClick}
        position="absolute"
        rounded="full"
        size="xs"
        variant="outline"
      >
        {direction === "bottom" ? <FiChevronDown /> : <FiChevronUp />}
      </IconButton>
    </Tooltip>
  );
};

export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: TaskLogContentProps) => {
  const hash = location.hash.replace("#", "");
  const parentRef = useRef<HTMLDivElement | null>(null);

  // Track whether user is at the bottom so we don't hijack their scroll position
  // if they scrolled up to read something
  const isAtBottomRef = useRef<boolean>(true);
  // Track previous log count to detect new lines arriving
  const prevLogCountRef = useRef<number>(0);

  const rowVirtualizer = useVirtualizer({
    count: parsedLogs.length,
    estimateSize: () => 20,
    getScrollElement: () => parentRef.current,
    overscan: 10,
  });

  const contentHeight = rowVirtualizer.getTotalSize();
  const containerHeight = rowVirtualizer.scrollElement?.clientHeight ?? 0;
  const showScrollButtons = parsedLogs.length > 1 && contentHeight > containerHeight;

  // Check if user is near the bottom on scroll
  const handleScroll = useCallback(() => {
    const el = parentRef.current;

    if (!el) {
      return;
    }
    const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;

    isAtBottomRef.current = distanceFromBottom <= SCROLL_BOTTOM_THRESHOLD;
  }, []);

  useEffect(() => {
    const el = parentRef.current;

    el?.addEventListener("scroll", handleScroll, { passive: true });

    return () => el?.removeEventListener("scroll", handleScroll);
  }, [handleScroll]);

  // Auto-scroll to bottom when:
  // 1. Logs first load (prevLogCount was 0)
  // 2. New lines arrive AND user was already at the bottom
  useLayoutEffect(() => {
    if (parsedLogs.length === 0) {
      return;
    }

    const isFirstLoad = prevLogCountRef.current === 0;
    const hasNewLines = parsedLogs.length > prevLogCountRef.current;

    if ((isFirstLoad || (hasNewLines && isAtBottomRef.current)) && !location.hash) {
      rowVirtualizer.scrollToIndex(parsedLogs.length - 1, { align: "end" });
    }

    prevLogCountRef.current = parsedLogs.length;
  }, [parsedLogs.length, rowVirtualizer]);

  useLayoutEffect(() => {
    if (location.hash && !isLoading) {
      rowVirtualizer.scrollToIndex(Math.min(Number(hash) + 5, parsedLogs.length - 1));
    }
  }, [isLoading, rowVirtualizer, hash, parsedLogs]);

  const handleScrollTo = (to: "bottom" | "top") => {
    if (parsedLogs.length === 0) {
      return;
    }

    const el = rowVirtualizer.scrollElement ?? parentRef.current;

    if (!el) {
      return;
    }

    if (to === "top") {
      isAtBottomRef.current = false;
      scrollToTop({ element: el, virtualizer: rowVirtualizer });
    } else {
      isAtBottomRef.current = true;
      scrollToBottom({ element: el, virtualizer: rowVirtualizer });
    }
  };

  useHotkeys("mod+ArrowDown", () => handleScrollTo("bottom"), { enabled: !isLoading });
  useHotkeys("mod+ArrowUp", () => handleScrollTo("top"), { enabled: !isLoading });

  return (
    <Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0} position="relative">
      <ErrorAlert error={error ?? logError} />
      <ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
      <Box
        data-testid="virtual-scroll-container"
        flexGrow={1}
        minHeight={0}
        overflow="auto"
        position="relative"
        py={3}
        ref={parentRef}
        width="100%"
      >
        <Code
          css={{
            "& *::selection": { bg: "blue.emphasized" },
          }}
          data-testid="virtualized-list"
          display="block"
          overflowX="auto"
          textWrap={wrap ? "pre" : "nowrap"}
          width="100%"
        >
          <VStack
            alignItems="flex-start"
            gap={0}
            h={`${rowVirtualizer.getTotalSize()}px`}
            position="relative"
          >
            {rowVirtualizer.getVirtualItems().map((virtualRow) => (
              <Box
                _ltr={{ left: 0, right: "auto" }}
                _rtl={{ left: "auto", right: 0 }}
                bgColor={
                  Boolean(hash) && virtualRow.index === Number(hash) - 1 ? "brand.emphasized" : "transparent"
                }
                data-index={virtualRow.index}
                data-testid={`virtualized-item-${virtualRow.index}`}
                key={virtualRow.key}
                position="absolute"
                ref={rowVirtualizer.measureElement}
                top={0}
                transform={`translateY(${virtualRow.start}px)`}
                width={wrap ? "100%" : "max-content"}
              >
                {parsedLogs[virtualRow.index] ?? undefined}
              </Box>
            ))}
          </VStack>
        </Code>
      </Box>

      {showScrollButtons ? (
        <>
          <ScrollToButton direction="top" onClick={() => handleScrollTo("top")} />
          <ScrollToButton direction="bottom" onClick={() => handleScrollTo("bottom")} />
        </>
      ) : undefined}
    </Box>
  );
};
