/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {Point} from './view-base';
import type {
  FlamechartStackFrame,
  NativeEvent,
  NetworkMeasure,
  ReactComponentMeasure,
  ReactHoverContextInfo,
  ReactMeasure,
  TimelineData,
  SchedulingEvent,
  Snapshot,
  SuspenseEvent,
  ThrownError,
  UserTimingMark,
} from './types';

import * as React from 'react';
import {formatDuration, formatTimestamp, trimString} from './utils/formatting';
import {getBatchRange} from './utils/getBatchRange';
import useSmartTooltip from './utils/useSmartTooltip';
import styles from './EventTooltip.css';

const MAX_TOOLTIP_TEXT_LENGTH = 60;

type Props = {|
  canvasRef: {|current: HTMLCanvasElement | null|},
  data: TimelineData,
  height: number,
  hoveredEvent: ReactHoverContextInfo | null,
  origin: Point,
  width: number,
|};

function getSchedulingEventLabel(event: SchedulingEvent): string | null {
  switch (event.type) {
    case 'schedule-render':
      return 'render scheduled';
    case 'schedule-state-update':
      return 'state update scheduled';
    case 'schedule-force-update':
      return 'force update scheduled';
    default:
      return null;
  }
}

function getReactMeasureLabel(type): string | null {
  switch (type) {
    case 'commit':
      return 'react commit';
    case 'render-idle':
      return 'react idle';
    case 'render':
      return 'react render';
    case 'layout-effects':
      return 'react layout effects';
    case 'passive-effects':
      return 'react passive effects';
    default:
      return null;
  }
}

export default function EventTooltip({
  canvasRef,
  data,
  height,
  hoveredEvent,
  origin,
  width,
}: Props) {
  const ref = useSmartTooltip({
    canvasRef,
    mouseX: origin.x,
    mouseY: origin.y,
  });

  if (hoveredEvent === null) {
    return null;
  }

  const {
    componentMeasure,
    flamechartStackFrame,
    measure,
    nativeEvent,
    networkMeasure,
    schedulingEvent,
    snapshot,
    suspenseEvent,
    thrownError,
    userTimingMark,
  } = hoveredEvent;

  let content = null;
  if (componentMeasure !== null) {
    content = (
      <TooltipReactComponentMeasure componentMeasure={componentMeasure} />
    );
  } else if (nativeEvent !== null) {
    content = <TooltipNativeEvent nativeEvent={nativeEvent} />;
  } else if (networkMeasure !== null) {
    content = <TooltipNetworkMeasure networkMeasure={networkMeasure} />;
  } else if (schedulingEvent !== null) {
    content = (
      <TooltipSchedulingEvent data={data} schedulingEvent={schedulingEvent} />
    );
  } else if (snapshot !== null) {
    content = (
      <TooltipSnapshot height={height} snapshot={snapshot} width={width} />
    );
  } else if (suspenseEvent !== null) {
    content = <TooltipSuspenseEvent suspenseEvent={suspenseEvent} />;
  } else if (measure !== null) {
    content = <TooltipReactMeasure data={data} measure={measure} />;
  } else if (flamechartStackFrame !== null) {
    content = <TooltipFlamechartNode stackFrame={flamechartStackFrame} />;
  } else if (userTimingMark !== null) {
    content = <TooltipUserTimingMark mark={userTimingMark} />;
  } else if (thrownError !== null) {
    content = <TooltipThrownError thrownError={thrownError} />;
  }

  if (content !== null) {
    return (
      <div className={styles.Tooltip} ref={ref}>
        {content}
      </div>
    );
  } else {
    return null;
  }
}

const TooltipReactComponentMeasure = ({
  componentMeasure,
}: {|
  componentMeasure: ReactComponentMeasure,
|}) => {
  const {componentName, duration, timestamp, type, warning} = componentMeasure;

  let label = componentName;
  switch (type) {
    case 'render':
      label += ' rendered';
      break;
    case 'layout-effect-mount':
      label += ' mounted layout effect';
      break;
    case 'layout-effect-unmount':
      label += ' unmounted layout effect';
      break;
    case 'passive-effect-mount':
      label += ' mounted passive effect';
      break;
    case 'passive-effect-unmount':
      label += ' unmounted passive effect';
      break;
  }

  return (
    <>
      <div className={styles.TooltipSection}>
        {trimString(label, 768)}
        <div className={styles.Divider} />
        <div className={styles.DetailsGrid}>
          <div className={styles.DetailsGridLabel}>Timestamp:</div>
          <div>{formatTimestamp(timestamp)}</div>
          <div className={styles.DetailsGridLabel}>Duration:</div>
          <div>{formatDuration(duration)}</div>
        </div>
      </div>
      {warning !== null && (
        <div className={styles.TooltipWarningSection}>
          <div className={styles.WarningText}>{warning}</div>
        </div>
      )}
    </>
  );
};

const TooltipFlamechartNode = ({
  stackFrame,
}: {|
  stackFrame: FlamechartStackFrame,
|}) => {
  const {name, timestamp, duration, locationLine, locationColumn} = stackFrame;
  return (
    <div className={styles.TooltipSection}>
      <span className={styles.FlamechartStackFrameName}>{name}</span>
      <div className={styles.DetailsGrid}>
        <div className={styles.DetailsGridLabel}>Timestamp:</div>
        <div>{formatTimestamp(timestamp)}</div>
        <div className={styles.DetailsGridLabel}>Duration:</div>
        <div>{formatDuration(duration)}</div>
        {(locationLine !== undefined || locationColumn !== undefined) && (
          <>
            <div className={styles.DetailsGridLabel}>Location:</div>
            <div>
              line {locationLine}, column {locationColumn}
            </div>
          </>
        )}
      </div>
    </div>
  );
};

const TooltipNativeEvent = ({nativeEvent}: {|nativeEvent: NativeEvent|}) => {
  const {duration, timestamp, type, warning} = nativeEvent;

  return (
    <>
      <div className={styles.TooltipSection}>
        <span className={styles.NativeEventName}>{trimString(type, 768)}</span>
        event
        <div className={styles.Divider} />
        <div className={styles.DetailsGrid}>
          <div className={styles.DetailsGridLabel}>Timestamp:</div>
          <div>{formatTimestamp(timestamp)}</div>
          <div className={styles.DetailsGridLabel}>Duration:</div>
          <div>{formatDuration(duration)}</div>
        </div>
      </div>
      {warning !== null && (
        <div className={styles.TooltipWarningSection}>
          <div className={styles.WarningText}>{warning}</div>
        </div>
      )}
    </>
  );
};

const TooltipNetworkMeasure = ({
  networkMeasure,
}: {|
  networkMeasure: NetworkMeasure,
|}) => {
  const {
    finishTimestamp,
    lastReceivedDataTimestamp,
    priority,
    sendRequestTimestamp,
    url,
  } = networkMeasure;

  let urlToDisplay = url;
  if (urlToDisplay.length > MAX_TOOLTIP_TEXT_LENGTH) {
    const half = Math.floor(MAX_TOOLTIP_TEXT_LENGTH / 2);
    urlToDisplay = url.substr(0, half) + '…' + url.substr(url.length - half);
  }

  const timestampBegin = sendRequestTimestamp;
  const timestampEnd = finishTimestamp || lastReceivedDataTimestamp;
  const duration =
    timestampEnd > 0
      ? formatDuration(finishTimestamp - timestampBegin)
      : '(incomplete)';

  return (
    <div className={styles.SingleLineTextSection}>
      {duration} <span className={styles.DimText}>{priority}</span>{' '}
      {urlToDisplay}
    </div>
  );
};

const TooltipSchedulingEvent = ({
  data,
  schedulingEvent,
}: {|
  data: TimelineData,
  schedulingEvent: SchedulingEvent,
|}) => {
  const label = getSchedulingEventLabel(schedulingEvent);
  if (!label) {
    if (__DEV__) {
      console.warn(
        'Unexpected schedulingEvent type "%s"',
        schedulingEvent.type,
      );
    }
    return null;
  }

  let laneLabels = null;
  let lanes = null;
  switch (schedulingEvent.type) {
    case 'schedule-render':
    case 'schedule-state-update':
    case 'schedule-force-update':
      lanes = schedulingEvent.lanes;
      laneLabels = lanes.map(
        lane => ((data.laneToLabelMap.get(lane): any): string),
      );
      break;
  }

  const {componentName, timestamp, warning} = schedulingEvent;

  return (
    <>
      <div className={styles.TooltipSection}>
        {componentName && (
          <span className={styles.ComponentName}>
            {trimString(componentName, 100)}
          </span>
        )}
        {label}
        <div className={styles.Divider} />
        <div className={styles.DetailsGrid}>
          {laneLabels !== null && lanes !== null && (
            <>
              <div className={styles.DetailsGridLabel}>Lanes:</div>
              <div>
                {laneLabels.join(', ')} ({lanes.join(', ')})
              </div>
            </>
          )}
          <div className={styles.DetailsGridLabel}>Timestamp:</div>
          <div>{formatTimestamp(timestamp)}</div>
        </div>
      </div>
      {warning !== null && (
        <div className={styles.TooltipWarningSection}>
          <div className={styles.WarningText}>{warning}</div>
        </div>
      )}
    </>
  );
};

const TooltipSnapshot = ({
  height,
  snapshot,
  width,
}: {|
  height: number,
  snapshot: Snapshot,
  width: number,
|}) => {
  const aspectRatio = snapshot.width / snapshot.height;

  // Zoomed in view should not be any bigger than the DevTools viewport.
  let safeWidth = snapshot.width;
  let safeHeight = snapshot.height;
  if (safeWidth > width) {
    safeWidth = width;
    safeHeight = safeWidth / aspectRatio;
  }
  if (safeHeight > height) {
    safeHeight = height;
    safeWidth = safeHeight * aspectRatio;
  }

  return (
    <img
      className={styles.Image}
      src={snapshot.imageSource}
      style={{height: safeHeight, width: safeWidth}}
    />
  );
};

const TooltipSuspenseEvent = ({
  suspenseEvent,
}: {|
  suspenseEvent: SuspenseEvent,
|}) => {
  const {
    componentName,
    duration,
    phase,
    promiseName,
    resolution,
    timestamp,
    warning,
  } = suspenseEvent;

  let label = 'suspended';
  if (phase !== null) {
    label += ` during ${phase}`;
  }

  return (
    <>
      <div className={styles.TooltipSection}>
        {componentName && (
          <span className={styles.ComponentName}>
            {trimString(componentName, 100)}
          </span>
        )}
        {label}
        <div className={styles.Divider} />
        <div className={styles.DetailsGrid}>
          {promiseName !== null && (
            <>
              <div className={styles.DetailsGridLabel}>Resource:</div>
              <div className={styles.DetailsGridLongValue}>{promiseName}</div>
            </>
          )}
          <div className={styles.DetailsGridLabel}>Status:</div>
          <div>{resolution}</div>
          <div className={styles.DetailsGridLabel}>Timestamp:</div>
          <div>{formatTimestamp(timestamp)}</div>
          {duration !== null && (
            <>
              <div className={styles.DetailsGridLabel}>Duration:</div>
              <div>{formatDuration(duration)}</div>
            </>
          )}
        </div>
      </div>
      {warning !== null && (
        <div className={styles.TooltipWarningSection}>
          <div className={styles.WarningText}>{warning}</div>
        </div>
      )}
    </>
  );
};

const TooltipReactMeasure = ({
  data,
  measure,
}: {|
  data: TimelineData,
  measure: ReactMeasure,
|}) => {
  const label = getReactMeasureLabel(measure.type);
  if (!label) {
    if (__DEV__) {
      console.warn('Unexpected measure type "%s"', measure.type);
    }
    return null;
  }

  const {batchUID, duration, timestamp, lanes} = measure;
  const [startTime, stopTime] = getBatchRange(batchUID, data);

  const laneLabels = lanes.map(
    lane => ((data.laneToLabelMap.get(lane): any): string),
  );

  return (
    <div className={styles.TooltipSection}>
      <span className={styles.ReactMeasureLabel}>{label}</span>
      <div className={styles.Divider} />
      <div className={styles.DetailsGrid}>
        <div className={styles.DetailsGridLabel}>Timestamp:</div>
        <div>{formatTimestamp(timestamp)}</div>
        {measure.type !== 'render-idle' && (
          <>
            <div className={styles.DetailsGridLabel}>Duration:</div>
            <div>{formatDuration(duration)}</div>
          </>
        )}
        <div className={styles.DetailsGridLabel}>Batch duration:</div>
        <div>{formatDuration(stopTime - startTime)}</div>
        <div className={styles.DetailsGridLabel}>
          Lane{lanes.length === 1 ? '' : 's'}:
        </div>
        <div>
          {laneLabels.length > 0
            ? `${laneLabels.join(', ')} (${lanes.join(', ')})`
            : lanes.join(', ')}
        </div>
      </div>
    </div>
  );
};

const TooltipUserTimingMark = ({mark}: {|mark: UserTimingMark|}) => {
  const {name, timestamp} = mark;
  return (
    <div className={styles.TooltipSection}>
      <span className={styles.UserTimingLabel}>{name}</span>
      <div className={styles.Divider} />
      <div className={styles.DetailsGrid}>
        <div className={styles.DetailsGridLabel}>Timestamp:</div>
        <div>{formatTimestamp(timestamp)}</div>
      </div>
    </div>
  );
};

const TooltipThrownError = ({thrownError}: {|thrownError: ThrownError|}) => {
  const {componentName, message, phase, timestamp} = thrownError;
  const label = `threw an error during ${phase}`;
  return (
    <div className={styles.TooltipSection}>
      {componentName && (
        <span className={styles.ComponentName}>
          {trimString(componentName, 100)}
        </span>
      )}
      <span className={styles.UserTimingLabel}>{label}</span>
      <div className={styles.Divider} />
      <div className={styles.DetailsGrid}>
        <div className={styles.DetailsGridLabel}>Timestamp:</div>
        <div>{formatTimestamp(timestamp)}</div>
        {message !== '' && (
          <>
            <div className={styles.DetailsGridLabel}>Error:</div>
            <div>{message}</div>
          </>
        )}
      </div>
    </div>
  );
};
