import {createRef, Fragment, useEffect} from 'react';
import {RouteComponentProps} from 'react-router';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';

import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
import MeasurementsPanel from 'sentry/components/events/interfaces/spans/measurementsPanel';
import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
import {
  boundsGenerator,
  getMeasurements,
} from 'sentry/components/events/interfaces/spans/utils';
import Panel from 'sentry/components/panels/panel';
import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
import {
  DividerSpacer,
  ScrollbarContainer,
  VirtualScrollbar,
  VirtualScrollbarGrip,
} from 'sentry/components/performance/waterfall/miniHeader';
import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
import {tct} from 'sentry/locale';
import {Organization} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import EventView from 'sentry/utils/discover/eventView';
import toPercent from 'sentry/utils/number/toPercent';
import {
  TraceError,
  TraceFullDetailed,
  TraceMeta,
} from 'sentry/utils/performance/quickTrace/types';
import {
  TraceDetailBody,
  TraceViewContainer,
  TraceViewHeaderContainer,
} from 'sentry/views/performance/traceDetails/styles';
import TransactionGroup from 'sentry/views/performance/traceDetails/transactionGroup';
import {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types';
import {
  getTraceInfo,
  hasTraceData,
  isRootTransaction,
} from 'sentry/views/performance/traceDetails/utils';

import LimitExceededMessage from './limitExceededMessage';
import TraceNotFound from './traceNotFound';

type AccType = {
  lastIndex: number;
  numberOfHiddenTransactionsAbove: number;
  renderedChildren: React.ReactNode[];
};

type Props = Pick<RouteComponentProps<{}, {}>, 'location'> & {
  meta: TraceMeta | null;
  organization: Organization;
  traceEventView: EventView;
  traceSlug: string;
  traces: TraceFullDetailed[];
  filteredEventIds?: Set<string>;
  handleLimitChange?: (newLimit: number) => void;
  orphanErrors?: TraceError[];
  traceInfo?: TraceInfo;
};

function TraceHiddenMessage({
  isVisible,
  numberOfHiddenTransactionsAbove,
  numberOfHiddenErrorsAbove,
}: {
  isVisible: boolean;
  numberOfHiddenErrorsAbove: number;
  numberOfHiddenTransactionsAbove: number;
}) {
  if (
    !isVisible ||
    (numberOfHiddenTransactionsAbove < 1 && numberOfHiddenErrorsAbove < 1)
  ) {
    return null;
  }

  const numOfTransaction = <strong>{numberOfHiddenTransactionsAbove}</strong>;
  const numOfErrors = <strong>{numberOfHiddenErrorsAbove}</strong>;

  const hiddenTransactionsMessage =
    numberOfHiddenTransactionsAbove < 1
      ? ''
      : numberOfHiddenTransactionsAbove === 1
      ? tct('[numOfTransaction] hidden transaction', {
          numOfTransaction,
        })
      : tct('[numOfTransaction] hidden transactions', {
          numOfTransaction,
        });

  const hiddenErrorsMessage =
    numberOfHiddenErrorsAbove < 1
      ? ''
      : numberOfHiddenErrorsAbove === 1
      ? tct('[numOfErrors] hidden error', {
          numOfErrors,
        })
      : tct('[numOfErrors] hidden errors', {
          numOfErrors,
        });

  return (
    <MessageRow>
      <span key="trace-info-message">
        {hiddenTransactionsMessage}
        {hiddenErrorsMessage && hiddenTransactionsMessage && ', '}
        {hiddenErrorsMessage}
      </span>
    </MessageRow>
  );
}

function isRowVisible(
  row: TraceFullDetailed | TraceError,
  filteredEventIds?: Set<string>
): boolean {
  return filteredEventIds ? filteredEventIds.has(row.event_id) : true;
}

function generateBounds(traceInfo: TraceInfo) {
  return boundsGenerator({
    traceStartTimestamp: traceInfo.startTimestamp,
    traceEndTimestamp: traceInfo.endTimestamp,
    viewStart: 0,
    viewEnd: 1,
  });
}

export default function TraceView({
  location,
  meta,
  organization,
  traces,
  traceSlug,
  traceEventView,
  filteredEventIds,
  orphanErrors,
  handleLimitChange,
  ...props
}: Props) {
  const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
  const sentrySpan = sentryTransaction?.startChild({
    op: 'trace.render',
    description: 'trace-view-content',
  });
  const hasOrphanErrors = orphanErrors && orphanErrors.length > 0;
  const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0);

  useEffect(() => {
    trackAnalytics('performance_views.trace_view.view', {
      organization,
    });
  }, [organization]);

  function renderTransaction(
    transaction: TraceFullDetailed,
    {
      continuingDepths,
      isOrphan,
      isLast,
      index,
      numberOfHiddenTransactionsAbove,
      traceInfo,
      hasGuideAnchor,
    }: {
      continuingDepths: TreeDepth[];
      hasGuideAnchor: boolean;
      index: number;
      isLast: boolean;
      isOrphan: boolean;
      numberOfHiddenTransactionsAbove: number;
      traceInfo: TraceInfo;
    }
  ) {
    const {children, event_id: eventId} = transaction;
    // Add 1 to the generation to make room for the "root trace"
    const generation = transaction.generation + 1;

    const isVisible = isRowVisible(transaction, filteredEventIds);

    const accumulated: AccType = children.reduce(
      (acc: AccType, child: TraceFullDetailed, idx: number) => {
        const isLastChild = idx === children.length - 1;
        const hasChildren = child.children.length > 0;

        const result = renderTransaction(child, {
          continuingDepths:
            !isLastChild && hasChildren
              ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
              : continuingDepths,
          isOrphan,
          isLast: isLastChild,
          index: acc.lastIndex + 1,
          numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
          traceInfo,
          hasGuideAnchor: false,
        });

        acc.lastIndex = result.lastIndex;
        acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
        acc.renderedChildren.push(result.transactionGroup);

        return acc;
      },
      {
        renderedChildren: [],
        lastIndex: index,
        numberOfHiddenTransactionsAbove: isVisible
          ? 0
          : numberOfHiddenTransactionsAbove + 1,
      }
    );

    return {
      transactionGroup: (
        <Fragment key={eventId}>
          <TraceHiddenMessage
            isVisible={isVisible}
            numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
            numberOfHiddenErrorsAbove={0}
          />
          <TransactionGroup
            location={location}
            traceViewRef={traceViewRef}
            organization={organization}
            traceInfo={traceInfo}
            transaction={{
              ...transaction,
              generation,
            }}
            measurements={
              traces && traces.length > 0
                ? getMeasurements(traces[0], generateBounds(traceInfo))
                : undefined
            }
            generateBounds={generateBounds(traceInfo)}
            continuingDepths={continuingDepths}
            isOrphan={isOrphan}
            isLast={isLast}
            index={index}
            isVisible={isVisible}
            hasGuideAnchor={hasGuideAnchor}
            renderedChildren={accumulated.renderedChildren}
            barColor={pickBarColor(transaction['transaction.op'])}
          />
        </Fragment>
      ),
      lastIndex: accumulated.lastIndex,
      numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
    };
  }

  const traceViewRef = createRef<HTMLDivElement>();
  const virtualScrollbarContainerRef = createRef<HTMLDivElement>();

  if (!hasTraceData(traces, orphanErrors)) {
    return (
      <TraceNotFound
        meta={meta}
        traceEventView={traceEventView}
        traceSlug={traceSlug}
        location={location}
        organization={organization}
      />
    );
  }

  const traceInfo = props.traceInfo || getTraceInfo(traces);

  const accumulator: {
    index: number;
    numberOfHiddenTransactionsAbove: number;
    traceInfo: TraceInfo;
    transactionGroups: React.ReactNode[];
  } = {
    index: 1,
    numberOfHiddenTransactionsAbove: 0,
    traceInfo,
    transactionGroups: [],
  };

  let lastIndex: number = 0;
  const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
    (acc, trace, index) => {
      const isLastTransaction = index === traces.length - 1;
      const hasChildren = trace.children.length > 0;
      const isNextChildOrphaned =
        !isLastTransaction && traces[index + 1].parent_span_id !== null;

      const result = renderTransaction(trace, {
        ...acc,
        // if the root of a subtrace has a parent_span_id, then it must be an orphan
        isOrphan: !isRootTransaction(trace),
        isLast: isLastTransaction && !hasOrphanErrors,
        continuingDepths:
          (!isLastTransaction && hasChildren) || hasOrphanErrors
            ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
            : [],
        hasGuideAnchor: index === 0,
      });

      acc.index = result.lastIndex + 1;
      lastIndex = Math.max(lastIndex, result.lastIndex);
      acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
      acc.transactionGroups.push(result.transactionGroup);
      return acc;
    },
    accumulator
  );

  // Build transaction groups for orphan errors
  let numOfHiddenErrorsAbove = 0;
  let totalNumOfHiddenErrors = 0;
  if (hasOrphanErrors) {
    orphanErrors.forEach((error, index) => {
      const isLastError = index === orphanErrors.length - 1;
      const isVisible = isRowVisible(error, filteredEventIds);
      const currentHiddenCount = numOfHiddenErrorsAbove;

      if (!isVisible) {
        numOfHiddenErrorsAbove += 1;
        totalNumOfHiddenErrors += 1;
      } else {
        numOfHiddenErrorsAbove = 0;
      }

      transactionGroups.push(
        <Fragment key={error.event_id}>
          <TraceHiddenMessage
            isVisible={isVisible}
            numberOfHiddenTransactionsAbove={
              index === 0 ? numberOfHiddenTransactionsAbove : 0
            }
            numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
          />
          <TransactionGroup
            location={location}
            organization={organization}
            traceViewRef={traceViewRef}
            traceInfo={traceInfo}
            transaction={{
              ...error,
              generation: 1,
            }}
            generateBounds={generateBounds(traceInfo)}
            measurements={
              traces && traces.length > 0
                ? getMeasurements(traces[0], generateBounds(traceInfo))
                : undefined
            }
            continuingDepths={[]}
            isOrphan
            isLast={isLastError}
            index={lastIndex + index + 1}
            isVisible={isVisible}
            hasGuideAnchor={index === 0 && transactionGroups.length === 0}
            renderedChildren={[]}
          />
        </Fragment>
      );
    });
  }

  const bounds = generateBounds(traceInfo);
  const measurements =
    traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
      ? getMeasurements(traces[0], bounds)
      : undefined;

  const traceView = (
    <TraceDetailBody>
      <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
        <DividerHandlerManager.Consumer>
          {({dividerPosition}) => (
            <ScrollbarManager.Provider
              dividerPosition={dividerPosition}
              interactiveLayerRef={virtualScrollbarContainerRef}
              isEmbedded
            >
              <StyledTracePanel>
                <TraceViewHeaderContainer>
                  <ScrollbarManager.Consumer>
                    {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
                      return (
                        <ScrollbarContainer
                          ref={virtualScrollbarContainerRef}
                          style={{
                            // the width of this component is shrunk to compensate for half of the width of the divider line
                            width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
                          }}
                          onScroll={onScroll}
                        >
                          <div
                            style={{
                              width: 0,
                              height: '1px',
                            }}
                            ref={scrollBarAreaRef}
                          />
                          <VirtualScrollbar
                            data-type="virtual-scrollbar"
                            ref={virtualScrollbarRef}
                            onMouseDown={onDragStart}
                          >
                            <VirtualScrollbarGrip />
                          </VirtualScrollbar>
                        </ScrollbarContainer>
                      );
                    }}
                  </ScrollbarManager.Consumer>
                  <DividerSpacer />
                  {measurements ? (
                    <MeasurementsPanel
                      measurements={measurements}
                      generateBounds={bounds}
                      dividerPosition={dividerPosition}
                    />
                  ) : null}
                </TraceViewHeaderContainer>
                <TraceViewContainer ref={traceViewRef}>
                  <TransactionGroup
                    location={location}
                    organization={organization}
                    traceInfo={traceInfo}
                    transaction={{
                      traceSlug,
                      generation: 0,
                      'transaction.duration':
                        traceInfo.endTimestamp - traceInfo.startTimestamp,
                      children: traces,
                      start_timestamp: traceInfo.startTimestamp,
                      timestamp: traceInfo.endTimestamp,
                    }}
                    measurements={measurements}
                    generateBounds={bounds}
                    continuingDepths={[]}
                    isOrphan={false}
                    isLast={false}
                    index={0}
                    isVisible
                    hasGuideAnchor={false}
                    renderedChildren={transactionGroups}
                    barColor={pickBarColor('')}
                    onlyOrphanErrors={onlyOrphanErrors}
                    traceViewRef={traceViewRef}
                    numOfOrphanErrors={orphanErrors?.length}
                  />
                  <TraceHiddenMessage
                    isVisible
                    numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
                    numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
                  />
                  <LimitExceededMessage
                    traceInfo={traceInfo}
                    organization={organization}
                    traceEventView={traceEventView}
                    meta={meta}
                    handleLimitChange={handleLimitChange}
                  />
                </TraceViewContainer>
              </StyledTracePanel>
            </ScrollbarManager.Provider>
          )}
        </DividerHandlerManager.Consumer>
      </DividerHandlerManager.Provider>
    </TraceDetailBody>
  );

  sentrySpan?.finish();

  return traceView;
}

export const StyledTracePanel = styled(Panel)`
  height: 100%;
  overflow-x: visible;

  ${TraceViewContainer} {
    overflow-x: visible;
  }
`;
