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

import {createDataSource, createState, PluginClient} from 'flipper-plugin';
import {
  Events,
  FrameScanEvent,
  FrameworkEventType,
  Id,
  Metadata,
  MetadataId,
  PerformanceStatsEvent,
  SnapshotInfo,
  ClientNode,
  FrameworkEventMetadata,
} from './ClientTypes';
import {
  UIState,
  NodeSelection,
  TraversalMode,
  StreamState,
  ReadOnlyUIState,
  LiveClientState,
  WireFrameMode,
  AugmentedFrameworkEvent,
  StreamInterceptorEventEmitter,
} from './DesktopTypes';
import EventEmitter from 'eventemitter3';
import {addInterceptors} from './fb-stubs/StreamInterceptor';
import {prefetchSourceFileLocation} from './components/fb-stubs/IDEContextMenu';
import {checkFocusedNodeStillActive} from './plugin/ClientDataUtils';
import {uiActions} from './plugin/uiActions';
import {first} from 'lodash';
import {getNode} from './utils/map';

type TraversalModeChangeEvent = {
  mode: TraversalMode;
};

export type Methods = {
  onTraversalModeChange(params: TraversalModeChangeEvent): Promise<void>;
};

export function plugin(client: PluginClient<Events, Methods>) {
  const rootId = createState<Id | undefined>(undefined);
  const metadata = createState<Map<MetadataId, Metadata>>(new Map());

  const streamInterceptor = new EventEmitter() as StreamInterceptorEventEmitter;
  addInterceptors(client.device.os, streamInterceptor);
  const snapshot = createState<SnapshotInfo | null>(null);
  const nodesAtom = createState<Map<Id, ClientNode>>(new Map());
  const frameworkEvents = createDataSource<AugmentedFrameworkEvent>([], {
    indices: [['nodeId']],
    limit: 10000,
  });
  const frameworkEventsCustomColumns = createState<Set<string>>(new Set());

  const frameworkEventMetadata = createState<
    Map<FrameworkEventType, FrameworkEventMetadata>
  >(new Map());

  const uiState: UIState = createUIState();

  //this is the client data is what drives all of desktop UI
  //it is always up-to-date with the client regardless of whether we are paused or not
  const mutableLiveClientData: LiveClientState = {
    snapshotInfo: null,
    nodes: new Map(),
  };

  let lastProcessedFrameTime = 0;

  const _uiActions = uiActions(
    uiState,
    nodesAtom,
    snapshot,
    mutableLiveClientData,
  );

  const perfEvents = createDataSource<PerformanceStatsEvent, 'txId'>([], {
    key: 'txId',
    limit: 10 * 1024,
  });

  client.onMessage('init', (event) => {
    console.log('[ui-debugger] init');
    rootId.set(event.rootId);
    uiState.frameworkEventMonitoring.update((draft) => {
      event.frameworkEventMetadata?.forEach((frameworkEventMeta) => {
        draft.set(frameworkEventMeta.type, false);
      });
    });
    if (
      event.supportedTraversalModes &&
      event.supportedTraversalModes.length > 1
    ) {
      uiState.supportedTraversalModes.set(event.supportedTraversalModes);
    }
    if (
      event.currentTraversalMode &&
      uiState.supportedTraversalModes.get().includes(event.currentTraversalMode)
    ) {
      uiState.currentTraversalMode.set(event.currentTraversalMode);
      console.log(
        `[ui-debugger] Unsupported debugger mode ${event.currentTraversalMode}.`,
      );
    }

    frameworkEventMetadata.update((draft) => {
      event.frameworkEventMetadata?.forEach((frameworkEventMeta) => {
        draft.set(frameworkEventMeta.type, frameworkEventMeta);
      });
    });
  });

  client.onConnect(() => {
    uiState.isConnected.set(true);
    console.log('[ui-debugger] connected');
  });

  client.onDisconnect(() => {
    uiState.isConnected.set(false);
    console.log('[ui-debugger] disconnected');
  });

  client.onMessage('metadataUpdate', async (event) => {
    if (!event.attributeMetadata) {
      return;
    }
    const metadata = Object.values(event.attributeMetadata);
    streamInterceptor.emit('metadataReceived', metadata);
  });

  streamInterceptor.on('metadataUpdated', (updatedMetadata) => {
    metadata.update((draft) => {
      for (const meta of updatedMetadata) {
        draft.set(meta.id, meta);
      }
    });
  });

  /**
   * The message handling below is a temporary measure for a couple of weeks until
   * clients migrate to the newer message/format.
   */
  client.onMessage('perfStats', (event) => {
    const stat = {
      txId: event.txId,
      observerType: event.observerType,
      nodesCount: event.nodesCount,
      start: event.start,
      traversalMS: event.traversalComplete - event.start,
      snapshotMS: event.snapshotComplete - event.traversalComplete,
      queuingMS: event.queuingComplete - event.snapshotComplete,
      deferredComputationMS:
        event.deferredComputationComplete - event.queuingComplete,
      serializationMS:
        event.serializationComplete - event.deferredComputationComplete,
      socketMS: event.socketComplete - event.serializationComplete,
    };
    client.logger.track('performance', 'subtreeUpdate', stat, 'ui-debugger');
    perfEvents.append(stat);
  });
  client.onMessage('performanceStats', (event) => {
    client.logger.track('performance', 'subtreeUpdate', event, 'ui-debugger');
    perfEvents.append(event);
  });

  const processFrame = async (frameScan: FrameScanEvent) => {
    const nodes = new Map(frameScan.nodes.map((node) => [node.id, {...node}]));

    streamInterceptor.emit('frameReceived', {
      frameTime: frameScan.frameTime,
      snapshot: frameScan.snapshot,
      nodes: nodes,
    });
    applyFrameworkEvents(frameScan, nodes);
  };

  streamInterceptor.on('frameUpdated', (frame) => {
    if (frame.frameTime > lastProcessedFrameTime) {
      applyFrameData(frame.nodes, frame.snapshot);
      lastProcessedFrameTime = frame.frameTime;
      const selectedNode = uiState.selectedNode.get();
      if (selectedNode != null)
        _uiActions.ensureAncestorsExpanded(selectedNode.id);
    }
  });

  function applyFrameworkEvents(
    frameScan: FrameScanEvent,
    nodes: Map<Id, ClientNode>,
  ) {
    const customColumns = frameworkEventsCustomColumns.get();
    for (const frameworkEvent of frameScan.frameworkEvents ?? []) {
      for (const key in frameworkEvent.payload) {
        customColumns.add(key);
      }

      const treeRoot = getNode(frameworkEvent.treeId, nodes);

      const treeRootFirstChild = getNode(first(treeRoot?.children), nodes);
      frameworkEvents.append({
        ...frameworkEvent,
        nodeName: nodes.get(frameworkEvent.nodeId)?.name,
        rootComponentName: treeRootFirstChild?.name,
      });
    }
    frameworkEventsCustomColumns.set(customColumns);

    if (uiState.isPaused.get() === true) {
      return;
    }

    const monitoredEvents = uiState.frameworkEventMonitoring.get();

    const filterMainThread = uiState.filterMainThreadMonitoring.get();

    const nodesToHighlight =
      frameScan.frameworkEvents
        ?.filter(
          (frameworkEvent) => monitoredEvents.get(frameworkEvent.type) === true,
        )
        .filter(
          (frameworkEvent) =>
            filterMainThread === false || frameworkEvent.thread === 'main',
        )
        .map((event) => event.nodeId) ?? [];

    uiState.highlightedNodes.update((draft) => {
      for (const node of nodesToHighlight) {
        draft.add(node);
      }
    });

    setTimeout(() => {
      uiState.highlightedNodes.update((draft) => {
        for (const nodeId of nodesToHighlight) {
          draft.delete(nodeId);
        }
      });
    }, HighlightTime);
  }

  function applyFrameData(
    nodes: Map<Id, ClientNode>,
    snapshotInfo: SnapshotInfo | undefined,
  ) {
    if (snapshotInfo) {
      mutableLiveClientData.snapshotInfo = snapshotInfo;
    }
    mutableLiveClientData.nodes = nodes;

    if (!uiState.isPaused.get()) {
      nodesAtom.set(mutableLiveClientData.nodes);
      snapshot.set(mutableLiveClientData.snapshotInfo);

      checkFocusedNodeStillActive(uiState, nodesAtom.get());
    }
    setTimeout(() => {
      //let react render, this can happen async
      for (const node of nodes.values()) {
        prefetchSourceFileLocation(node);
      }
    }, 0);
  }

  client.onMessage('subtreeUpdate', (subtreeUpdate) => {
    processFrame({
      frameTime: subtreeUpdate.txId,
      nodes: subtreeUpdate.nodes,
      snapshot: {data: subtreeUpdate.snapshot, nodeId: subtreeUpdate.rootId},
      frameworkEvents: subtreeUpdate.frameworkEvents,
    });
  });
  client.onMessage('frameScan', processFrame);

  const onTraversalModeChange = async (mode: TraversalMode) =>
    client.send('onTraversalModeChange', {mode});

  return {
    rootId,
    uiState: uiState as ReadOnlyUIState,
    uiActions: _uiActions,
    nodes: nodesAtom,
    frameworkEvents,
    frameworkEventMetadata,
    frameworkEventsCustomColumns,
    snapshot,
    metadata,
    perfEvents,
    os: client.device.os,
    onTraversalModeChange,
  };
}

const HighlightTime = 300;

export {Component} from './components/main';
export * from './ClientTypes';

function createUIState(): UIState {
  return {
    isConnected: createState(false),

    viewMode: createState({mode: 'default'}),

    //used to disabled hover effects which cause rerenders and mess up the existing context menu
    isContextMenuOpen: createState<boolean>(false),

    streamState: createState<StreamState>({state: 'Ok'}),
    visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)),

    highlightedNodes: createState(new Set<Id>()),

    selectedNode: createState<NodeSelection | undefined>(undefined),
    //used to indicate whether we will higher the visualizer / tree when a matching event comes in
    //also whether or not will show running total  in the tree
    frameworkEventMonitoring: createState(
      new Map<FrameworkEventType, boolean>(),
    ),
    filterMainThreadMonitoring: createState(false),

    isPaused: createState(false),

    //The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
    //The nodes are sorted by area since you most likely want to select the smallest node under your cursor
    hoveredNodes: createState<Id[]>([]),

    searchTerm: createState<string>(''),
    focusedNode: createState<Id | undefined>(undefined),
    expandedNodes: createState<Set<Id>>(new Set()),
    wireFrameMode: createState<WireFrameMode>('All'),

    // view-hierarchy is the default state so we start with it until we fetch supported modes from the client
    supportedTraversalModes: createState<TraversalMode[]>(['view-hierarchy']),
    currentTraversalMode: createState<TraversalMode>('view-hierarchy'),
  };
}
