/**
 * 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 {PROFILER_EXPORT_VERSION} from 'react-devtools-shared/src/constants';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';

import type {ProfilingDataBackend} from 'react-devtools-shared/src/backend/types';
import type {
  ProfilingDataExport,
  ProfilingDataForRootExport,
  ProfilingDataForRootFrontend,
  ProfilingDataFrontend,
  SnapshotNode,
} from './types';
import type {
  TimelineData,
  TimelineDataExport,
} from 'react-devtools-timeline/src/types';

const commitGradient = [
  'var(--color-commit-gradient-0)',
  'var(--color-commit-gradient-1)',
  'var(--color-commit-gradient-2)',
  'var(--color-commit-gradient-3)',
  'var(--color-commit-gradient-4)',
  'var(--color-commit-gradient-5)',
  'var(--color-commit-gradient-6)',
  'var(--color-commit-gradient-7)',
  'var(--color-commit-gradient-8)',
  'var(--color-commit-gradient-9)',
];

// Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI.
// This format can then be quickly exported (and re-imported).
export function prepareProfilingDataFrontendFromBackendAndStore(
  dataBackends: Array<ProfilingDataBackend>,
  operationsByRootID: Map<number, Array<Array<number>>>,
  snapshotsByRootID: Map<number, Map<number, SnapshotNode>>,
): ProfilingDataFrontend {
  const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();

  const timelineDataArray = [];

  dataBackends.forEach(dataBackend => {
    const {timelineData} = dataBackend;
    if (timelineData != null) {
      const {
        batchUIDToMeasuresKeyValueArray,
        internalModuleSourceToRanges,
        laneToLabelKeyValueArray,
        laneToReactMeasureKeyValueArray,
        ...rest
      } = timelineData;

      timelineDataArray.push({
        ...rest,

        // Most of the data is safe to parse as-is,
        // but we need to convert the nested Arrays back to Maps.
        batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
        internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
        laneToLabelMap: new Map(laneToLabelKeyValueArray),
        laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
      });
    }

    dataBackend.dataForRoots.forEach(
      ({commitData, displayName, initialTreeBaseDurations, rootID}) => {
        const operations = operationsByRootID.get(rootID);
        if (operations == null) {
          throw Error(
            `Could not find profiling operations for root "${rootID}"`,
          );
        }

        const snapshots = snapshotsByRootID.get(rootID);
        if (snapshots == null) {
          throw Error(
            `Could not find profiling snapshots for root "${rootID}"`,
          );
        }

        // Do not filter empty commits from the profiler data!
        // Hiding "empty" commits might cause confusion too.
        // A commit *did happen* even if none of the components the Profiler is showing were involved.
        const convertedCommitData = commitData.map(
          (commitDataBackend, commitIndex) => ({
            changeDescriptions:
              commitDataBackend.changeDescriptions != null
                ? new Map(commitDataBackend.changeDescriptions)
                : null,
            duration: commitDataBackend.duration,
            effectDuration: commitDataBackend.effectDuration,
            fiberActualDurations: new Map(
              commitDataBackend.fiberActualDurations,
            ),
            fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
            passiveEffectDuration: commitDataBackend.passiveEffectDuration,
            priorityLevel: commitDataBackend.priorityLevel,
            timestamp: commitDataBackend.timestamp,
            updaters:
              commitDataBackend.updaters !== null
                ? commitDataBackend.updaters.map(serializedElement => {
                    const [
                      serializedElementDisplayName,
                      serializedElementHocDisplayNames,
                    ] = separateDisplayNameAndHOCs(
                      serializedElement.displayName,
                      serializedElement.type,
                    );
                    return {
                      ...serializedElement,
                      displayName: serializedElementDisplayName,
                      hocDisplayNames: serializedElementHocDisplayNames,
                    };
                  })
                : null,
          }),
        );

        dataForRoots.set(rootID, {
          commitData: convertedCommitData,
          displayName,
          initialTreeBaseDurations: new Map(initialTreeBaseDurations),
          operations,
          rootID,
          snapshots,
        });
      },
    );
  });

  return {dataForRoots, imported: false, timelineData: timelineDataArray};
}

// Converts a Profiling data export into the format required by the Store.
export function prepareProfilingDataFrontendFromExport(
  profilingDataExport: ProfilingDataExport,
): ProfilingDataFrontend {
  const {version} = profilingDataExport;

  if (version !== PROFILER_EXPORT_VERSION) {
    throw Error(
      `Unsupported profile export version "${version}". Supported version is "${PROFILER_EXPORT_VERSION}".`,
    );
  }

  const timelineData: Array<TimelineData> = profilingDataExport.timelineData
    ? profilingDataExport.timelineData.map(
        ({
          batchUIDToMeasuresKeyValueArray,
          componentMeasures,
          duration,
          flamechart,
          internalModuleSourceToRanges,
          laneToLabelKeyValueArray,
          laneToReactMeasureKeyValueArray,
          nativeEvents,
          networkMeasures,
          otherUserTimingMarks,
          reactVersion,
          schedulingEvents,
          snapshots,
          snapshotHeight,
          startTime,
          suspenseEvents,
          thrownErrors,
        }) => ({
          // Most of the data is safe to parse as-is,
          // but we need to convert the nested Arrays back to Maps.
          batchUIDToMeasuresMap: new Map(batchUIDToMeasuresKeyValueArray),
          componentMeasures,
          duration,
          flamechart,
          internalModuleSourceToRanges: new Map(internalModuleSourceToRanges),
          laneToLabelMap: new Map(laneToLabelKeyValueArray),
          laneToReactMeasureMap: new Map(laneToReactMeasureKeyValueArray),
          nativeEvents,
          networkMeasures,
          otherUserTimingMarks,
          reactVersion,
          schedulingEvents,
          snapshots,
          snapshotHeight,
          startTime,
          suspenseEvents,
          thrownErrors,
        }),
      )
    : [];

  const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
  profilingDataExport.dataForRoots.forEach(
    ({
      commitData,
      displayName,
      initialTreeBaseDurations,
      operations,
      rootID,
      snapshots,
    }) => {
      dataForRoots.set(rootID, {
        commitData: commitData.map(
          ({
            changeDescriptions,
            duration,
            effectDuration,
            fiberActualDurations,
            fiberSelfDurations,
            passiveEffectDuration,
            priorityLevel,
            timestamp,
            updaters,
          }) => ({
            changeDescriptions:
              changeDescriptions != null ? new Map(changeDescriptions) : null,
            duration,
            effectDuration,
            fiberActualDurations: new Map(fiberActualDurations),
            fiberSelfDurations: new Map(fiberSelfDurations),
            passiveEffectDuration,
            priorityLevel,
            timestamp,
            updaters,
          }),
        ),
        displayName,
        initialTreeBaseDurations: new Map(initialTreeBaseDurations),
        operations,
        rootID,
        snapshots: new Map(snapshots),
      });
    },
  );

  return {
    dataForRoots,
    imported: true,
    timelineData,
  };
}

// Converts a Store Profiling data into a format that can be safely (JSON) serialized for export.
export function prepareProfilingDataExport(
  profilingDataFrontend: ProfilingDataFrontend,
): ProfilingDataExport {
  const timelineData: Array<TimelineDataExport> = profilingDataFrontend.timelineData.map(
    ({
      batchUIDToMeasuresMap,
      componentMeasures,
      duration,
      flamechart,
      internalModuleSourceToRanges,
      laneToLabelMap,
      laneToReactMeasureMap,
      nativeEvents,
      networkMeasures,
      otherUserTimingMarks,
      reactVersion,
      schedulingEvents,
      snapshots,
      snapshotHeight,
      startTime,
      suspenseEvents,
      thrownErrors,
    }) => ({
      // Most of the data is safe to serialize as-is,
      // but we need to convert the Maps to nested Arrays.
      batchUIDToMeasuresKeyValueArray: Array.from(
        batchUIDToMeasuresMap.entries(),
      ),
      componentMeasures: componentMeasures,
      duration,
      flamechart,
      internalModuleSourceToRanges: Array.from(
        internalModuleSourceToRanges.entries(),
      ),
      laneToLabelKeyValueArray: Array.from(laneToLabelMap.entries()),
      laneToReactMeasureKeyValueArray: Array.from(
        laneToReactMeasureMap.entries(),
      ),
      nativeEvents,
      networkMeasures,
      otherUserTimingMarks,
      reactVersion,
      schedulingEvents,
      snapshots,
      snapshotHeight,
      startTime,
      suspenseEvents,
      thrownErrors,
    }),
  );

  const dataForRoots: Array<ProfilingDataForRootExport> = [];
  profilingDataFrontend.dataForRoots.forEach(
    ({
      commitData,
      displayName,
      initialTreeBaseDurations,
      operations,
      rootID,
      snapshots,
    }) => {
      dataForRoots.push({
        commitData: commitData.map(
          ({
            changeDescriptions,
            duration,
            effectDuration,
            fiberActualDurations,
            fiberSelfDurations,
            passiveEffectDuration,
            priorityLevel,
            timestamp,
            updaters,
          }) => ({
            changeDescriptions:
              changeDescriptions != null
                ? Array.from(changeDescriptions.entries())
                : null,
            duration,
            effectDuration,
            fiberActualDurations: Array.from(fiberActualDurations.entries()),
            fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
            passiveEffectDuration,
            priorityLevel,
            timestamp,
            updaters,
          }),
        ),
        displayName,
        initialTreeBaseDurations: Array.from(
          initialTreeBaseDurations.entries(),
        ),
        operations,
        rootID,
        snapshots: Array.from(snapshots.entries()),
      });
    },
  );

  return {
    version: PROFILER_EXPORT_VERSION,
    dataForRoots,
    timelineData,
  };
}

export const getGradientColor = (value: number) => {
  const maxIndex = commitGradient.length - 1;
  let index;
  if (Number.isNaN(value)) {
    index = 0;
  } else if (!Number.isFinite(value)) {
    index = maxIndex;
  } else {
    index = Math.max(0, Math.min(maxIndex, value)) * maxIndex;
  }
  return commitGradient[Math.round(index)];
};

export const formatDuration = (duration: number) =>
  Math.round(duration * 10) / 10 || '<0.1';
export const formatPercentage = (percentage: number) =>
  Math.round(percentage * 100);
export const formatTime = (timestamp: number) =>
  Math.round(Math.round(timestamp) / 100) / 10;

export const scale = (
  minValue: number,
  maxValue: number,
  minRange: number,
  maxRange: number,
) => (value: number, fallbackValue: number) =>
  maxValue - minValue === 0
    ? fallbackValue
    : ((value - minValue) / (maxValue - minValue)) * (maxRange - minRange);
