// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @file
 *
 * This class encapsulates the type-related validation logic for moving timing information for nodes
 * through the different simulation phases. Methods here ensure that the invariants of simulation hold
 * as nodes are queued, partially simulated, and completed.
 */

import * as Core from '../core/core.js';
import * as Graph from '../graph/graph.js';

interface NodeTimingComplete {
  startTime: number;
  endTime: number;
  queuedTime: number;
  estimatedTimeElapsed: number;
  timeElapsed: number;
  timeElapsedOvershoot: number;
  bytesDownloaded: number;
}

type NodeTimingQueued = Pick<NodeTimingComplete, 'queuedTime'>;

type CpuNodeTimingStarted = NodeTimingQueued&Pick<NodeTimingComplete, 'startTime'|'timeElapsed'>;
type NetworkNodeTimingStarted = CpuNodeTimingStarted&Pick<NodeTimingComplete, 'timeElapsedOvershoot'|'bytesDownloaded'>;

type CpuNodeTimingInProgress = CpuNodeTimingStarted&Pick<NodeTimingComplete, 'estimatedTimeElapsed'>;
type NetworkNodeTimingInProgress = NetworkNodeTimingStarted&Pick<NodeTimingComplete, 'estimatedTimeElapsed'>;

export type CpuNodeTimingComplete = CpuNodeTimingInProgress&Pick<NodeTimingComplete, 'endTime'>;
export type NetworkNodeTimingComplete =
    NetworkNodeTimingInProgress&Pick<NodeTimingComplete, 'endTime'>&{connectionTiming: ConnectionTiming};
export type CompleteNodeTiming = CpuNodeTimingComplete|NetworkNodeTimingComplete;

type NodeTimingData = NodeTimingQueued|CpuNodeTimingStarted|NetworkNodeTimingStarted|CpuNodeTimingInProgress|
    NetworkNodeTimingInProgress|CpuNodeTimingComplete|NetworkNodeTimingComplete;

export interface ConnectionTiming {
  dnsResolutionTime?: number;
  connectionTime?: number;
  sslTime?: number;
  timeToFirstByte: number;
}

class SimulatorTimingMap {
  nodeTimings: Map<Graph.Node, NodeTimingData>;

  constructor() {
    this.nodeTimings = new Map<Graph.Node, NodeTimingData>();
  }

  getNodes(): Graph.Node[] {
    return Array.from(this.nodeTimings.keys());
  }

  setReadyToStart(node: Graph.Node, values: {queuedTime: number}): void {
    this.nodeTimings.set(node, values);
  }

  setInProgress(node: Graph.Node, values: {startTime: number}): void {
    const nodeTiming = {
      ...this.getQueued(node),
      startTime: values.startTime,
      timeElapsed: 0,
    };

    this.nodeTimings.set(
        node,
        node.type === Graph.BaseNode.types.NETWORK ? {...nodeTiming, timeElapsedOvershoot: 0, bytesDownloaded: 0} :
                                                     nodeTiming,
    );
  }

  setCompleted(node: Graph.Node, values: {endTime: number, connectionTiming?: ConnectionTiming}): void {
    const nodeTiming = {
      ...this.getInProgress(node),
      endTime: values.endTime,
      connectionTiming: values.connectionTiming,
    };

    this.nodeTimings.set(node, nodeTiming);
  }

  setCpu(node: Graph.CPUNode, values: {timeElapsed: number}): void {
    const nodeTiming = {
      ...this.getCpuStarted(node),
      timeElapsed: values.timeElapsed,
    };

    this.nodeTimings.set(node, nodeTiming);
  }

  setCpuEstimated(node: Graph.CPUNode, values: {estimatedTimeElapsed: number}): void {
    const nodeTiming = {
      ...this.getCpuStarted(node),
      estimatedTimeElapsed: values.estimatedTimeElapsed,
    };

    this.nodeTimings.set(node, nodeTiming);
  }

  setNetwork(
      node: Graph.NetworkNode,
      values: {timeElapsed: number, timeElapsedOvershoot: number, bytesDownloaded: number}): void {
    const nodeTiming = {
      ...this.getNetworkStarted(node),
      timeElapsed: values.timeElapsed,
      timeElapsedOvershoot: values.timeElapsedOvershoot,
      bytesDownloaded: values.bytesDownloaded,
    };

    this.nodeTimings.set(node, nodeTiming);
  }

  setNetworkEstimated(node: Graph.NetworkNode, values: {estimatedTimeElapsed: number}): void {
    const nodeTiming = {
      ...this.getNetworkStarted(node),
      estimatedTimeElapsed: values.estimatedTimeElapsed,
    };

    this.nodeTimings.set(node, nodeTiming);
  }

  getQueued(node: Graph.Node): NodeTimingData {
    const timing = this.nodeTimings.get(node);
    if (!timing) {
      throw new Core.LanternError(`Node ${node.id} not yet queued`);
    }
    return timing;
  }

  getCpuStarted(node: Graph.CPUNode): CpuNodeTimingStarted {
    const timing = this.nodeTimings.get(node);
    if (!timing) {
      throw new Core.LanternError(`Node ${node.id} not yet queued`);
    }
    if (!('startTime' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet started`);
    }
    if ('bytesDownloaded' in timing) {
      throw new Core.LanternError(`Node ${node.id} timing not valid`);
    }
    return timing;
  }

  getNetworkStarted(node: Graph.NetworkNode): NetworkNodeTimingStarted {
    const timing = this.nodeTimings.get(node);
    if (!timing) {
      throw new Core.LanternError(`Node ${node.id} not yet queued`);
    }
    if (!('startTime' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet started`);
    }
    if (!('bytesDownloaded' in timing)) {
      throw new Core.LanternError(`Node ${node.id} timing not valid`);
    }
    return timing;
  }

  getInProgress(node: Graph.Node): CpuNodeTimingInProgress|NetworkNodeTimingInProgress {
    const timing = this.nodeTimings.get(node);
    if (!timing) {
      throw new Core.LanternError(`Node ${node.id} not yet queued`);
    }
    if (!('startTime' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet started`);
    }
    if (!('estimatedTimeElapsed' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet in progress`);
    }
    return timing;
  }

  getCompleted(node: Graph.Node): CpuNodeTimingComplete|NetworkNodeTimingComplete {
    const timing = this.nodeTimings.get(node);
    if (!timing) {
      throw new Core.LanternError(`Node ${node.id} not yet queued`);
    }
    if (!('startTime' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet started`);
    }
    if (!('estimatedTimeElapsed' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet in progress`);
    }
    if (!('endTime' in timing)) {
      throw new Core.LanternError(`Node ${node.id} not yet completed`);
    }
    return timing;
  }
}

export {SimulatorTimingMap};
