'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

// eslint-disable-next-line
const CodePathSegment = require('./code-path-segment');
// eslint-disable-next-line
const ForkContext = require('./fork-context');

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Adds given segments into the `dest` array.
 * If the `others` array does not includes the given segments, adds to the `all`
 * array as well.
 *
 * This adds only reachable and used segments.
 * @param {CodePathSegment[]} dest A destination array (`returnedSegments` or `thrownSegments`).
 * @param {CodePathSegment[]} others Another destination array (`returnedSegments` or `thrownSegments`).
 * @param {CodePathSegment[]} all The unified destination array (`finalSegments`).
 * @param {CodePathSegment[]} segments Segments to add.
 * @returns {void}
 */
function addToReturnedOrThrown(dest, others, all, segments) {
  for (let i = 0; i < segments.length; ++i) {
    const segment = segments[i];

    dest.push(segment);
    if (!others.includes(segment)) {
      all.push(segment);
    }
  }
}

/**
 * Gets a loop-context for a `continue` statement.
 * @param {CodePathState} state A state to get.
 * @param {string} label The label of a `continue` statement.
 * @returns {LoopContext} A loop-context for a `continue` statement.
 */
function getContinueContext(state, label) {
  if (!label) {
    return state.loopContext;
  }

  let context = state.loopContext;

  while (context) {
    if (context.label === label) {
      return context;
    }
    context = context.upper;
  }

  /* c8 ignore next */
  return null;
}

/**
 * Gets a context for a `break` statement.
 * @param {CodePathState} state A state to get.
 * @param {string} label The label of a `break` statement.
 * @returns {LoopContext|SwitchContext} A context for a `break` statement.
 */
function getBreakContext(state, label) {
  let context = state.breakContext;

  while (context) {
    if (label ? context.label === label : context.breakable) {
      return context;
    }
    context = context.upper;
  }

  /* c8 ignore next */
  return null;
}

/**
 * Gets a context for a `return` statement.
 * @param {CodePathState} state A state to get.
 * @returns {TryContext|CodePathState} A context for a `return` statement.
 */
function getReturnContext(state) {
  let context = state.tryContext;

  while (context) {
    if (context.hasFinalizer && context.position !== 'finally') {
      return context;
    }
    context = context.upper;
  }

  return state;
}

/**
 * Gets a context for a `throw` statement.
 * @param {CodePathState} state A state to get.
 * @returns {TryContext|CodePathState} A context for a `throw` statement.
 */
function getThrowContext(state) {
  let context = state.tryContext;

  while (context) {
    if (
      context.position === 'try' ||
      (context.hasFinalizer && context.position === 'catch')
    ) {
      return context;
    }
    context = context.upper;
  }

  return state;
}

/**
 * Removes a given element from a given array.
 * @param {any[]} xs An array to remove the specific element.
 * @param {any} x An element to be removed.
 * @returns {void}
 */
function remove(xs, x) {
  xs.splice(xs.indexOf(x), 1);
}

/**
 * Disconnect given segments.
 *
 * This is used in a process for switch statements.
 * If there is the "default" chunk before other cases, the order is different
 * between node's and running's.
 * @param {CodePathSegment[]} prevSegments Forward segments to disconnect.
 * @param {CodePathSegment[]} nextSegments Backward segments to disconnect.
 * @returns {void}
 */
function removeConnection(prevSegments, nextSegments) {
  for (let i = 0; i < prevSegments.length; ++i) {
    const prevSegment = prevSegments[i];
    const nextSegment = nextSegments[i];

    remove(prevSegment.nextSegments, nextSegment);
    remove(prevSegment.allNextSegments, nextSegment);
    remove(nextSegment.prevSegments, prevSegment);
    remove(nextSegment.allPrevSegments, prevSegment);
  }
}

/**
 * Creates looping path.
 * @param {CodePathState} state The instance.
 * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source.
 * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination.
 * @returns {void}
 */
function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) {
  const fromSegments = CodePathSegment.flattenUnusedSegments(
    unflattenedFromSegments,
  );
  const toSegments = CodePathSegment.flattenUnusedSegments(
    unflattenedToSegments,
  );

  const end = Math.min(fromSegments.length, toSegments.length);

  for (let i = 0; i < end; ++i) {
    const fromSegment = fromSegments[i];
    const toSegment = toSegments[i];

    if (toSegment.reachable) {
      fromSegment.nextSegments.push(toSegment);
    }
    if (fromSegment.reachable) {
      toSegment.prevSegments.push(fromSegment);
    }
    fromSegment.allNextSegments.push(toSegment);
    toSegment.allPrevSegments.push(fromSegment);

    if (toSegment.allPrevSegments.length >= 2) {
      CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment);
    }

    state.notifyLooped(fromSegment, toSegment);
  }
}

/**
 * Finalizes segments of `test` chunk of a ForStatement.
 *
 * - Adds `false` paths to paths which are leaving from the loop.
 * - Sets `true` paths to paths which go to the body.
 * @param {LoopContext} context A loop context to modify.
 * @param {ChoiceContext} choiceContext A choice context of this loop.
 * @param {CodePathSegment[]} head The current head paths.
 * @returns {void}
 */
function finalizeTestSegmentsOfFor(context, choiceContext, head) {
  if (!choiceContext.processed) {
    choiceContext.trueForkContext.add(head);
    choiceContext.falseForkContext.add(head);
    choiceContext.qqForkContext.add(head);
  }

  if (context.test !== true) {
    context.brokenForkContext.addAll(choiceContext.falseForkContext);
  }
  context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1);
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

/**
 * A class which manages state to analyze code paths.
 */
class CodePathState {
  /**
   * @param {IdGenerator} idGenerator An id generator to generate id for code
   *   path segments.
   * @param {Function} onLooped A callback function to notify looping.
   */
  constructor(idGenerator, onLooped) {
    this.idGenerator = idGenerator;
    this.notifyLooped = onLooped;
    this.forkContext = ForkContext.newRoot(idGenerator);
    this.choiceContext = null;
    this.switchContext = null;
    this.tryContext = null;
    this.loopContext = null;
    this.breakContext = null;
    this.chainContext = null;

    this.currentSegments = [];
    this.initialSegment = this.forkContext.head[0];

    // returnedSegments and thrownSegments push elements into finalSegments also.
    const final = (this.finalSegments = []);
    const returned = (this.returnedForkContext = []);
    const thrown = (this.thrownForkContext = []);

    returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final);
    thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final);
  }

  /**
   * The head segments.
   * @type {CodePathSegment[]}
   */
  get headSegments() {
    return this.forkContext.head;
  }

  /**
   * The parent forking context.
   * This is used for the root of new forks.
   * @type {ForkContext}
   */
  get parentForkContext() {
    const current = this.forkContext;

    return current && current.upper;
  }

  /**
   * Creates and stacks new forking context.
   * @param {boolean} forkLeavingPath A flag which shows being in a
   *   "finally" block.
   * @returns {ForkContext} The created context.
   */
  pushForkContext(forkLeavingPath) {
    this.forkContext = ForkContext.newEmpty(this.forkContext, forkLeavingPath);

    return this.forkContext;
  }

  /**
   * Pops and merges the last forking context.
   * @returns {ForkContext} The last context.
   */
  popForkContext() {
    const lastContext = this.forkContext;

    this.forkContext = lastContext.upper;
    this.forkContext.replaceHead(lastContext.makeNext(0, -1));

    return lastContext;
  }

  /**
   * Creates a new path.
   * @returns {void}
   */
  forkPath() {
    this.forkContext.add(this.parentForkContext.makeNext(-1, -1));
  }

  /**
   * Creates a bypass path.
   * This is used for such as IfStatement which does not have "else" chunk.
   * @returns {void}
   */
  forkBypassPath() {
    this.forkContext.add(this.parentForkContext.head);
  }

  //--------------------------------------------------------------------------
  // ConditionalExpression, LogicalExpression, IfStatement
  //--------------------------------------------------------------------------

  /**
   * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only),
   * IfStatement, WhileStatement, DoWhileStatement, or ForStatement.
   *
   * LogicalExpressions have cases that it goes different paths between the
   * `true` case and the `false` case.
   *
   * For Example:
   *
   *     if (a || b) {
   *         foo();
   *     } else {
   *         bar();
   *     }
   *
   * In this case, `b` is evaluated always in the code path of the `else`
   * block, but it's not so in the code path of the `if` block.
   * So there are 3 paths.
   *
   *     a -> foo();
   *     a -> b -> foo();
   *     a -> b -> bar();
   * @param {string} kind A kind string.
   *   If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`.
   *   If it's IfStatement's or ConditionalExpression's, this is `"test"`.
   *   Otherwise, this is `"loop"`.
   * @param {boolean} isForkingAsResult A flag that shows that goes different
   *   paths between `true` and `false`.
   * @returns {void}
   */
  pushChoiceContext(kind, isForkingAsResult) {
    this.choiceContext = {
      upper: this.choiceContext,
      kind,
      isForkingAsResult,
      trueForkContext: ForkContext.newEmpty(this.forkContext),
      falseForkContext: ForkContext.newEmpty(this.forkContext),
      qqForkContext: ForkContext.newEmpty(this.forkContext),
      processed: false,
    };
  }

  /**
   * Pops the last choice context and finalizes it.
   * @throws {Error} (Unreachable.)
   * @returns {ChoiceContext} The popped context.
   */
  popChoiceContext() {
    const context = this.choiceContext;

    this.choiceContext = context.upper;

    const forkContext = this.forkContext;
    const headSegments = forkContext.head;

    switch (context.kind) {
      case '&&':
      case '||':
      case '??':
        /*
         * If any result were not transferred from child contexts,
         * this sets the head segments to both cases.
         * The head segments are the path of the right-hand operand.
         */
        if (!context.processed) {
          context.trueForkContext.add(headSegments);
          context.falseForkContext.add(headSegments);
          context.qqForkContext.add(headSegments);
        }

        /*
         * Transfers results to upper context if this context is in
         * test chunk.
         */
        if (context.isForkingAsResult) {
          const parentContext = this.choiceContext;

          parentContext.trueForkContext.addAll(context.trueForkContext);
          parentContext.falseForkContext.addAll(context.falseForkContext);
          parentContext.qqForkContext.addAll(context.qqForkContext);
          parentContext.processed = true;

          return context;
        }

        break;

      case 'test':
        if (!context.processed) {
          /*
           * The head segments are the path of the `if` block here.
           * Updates the `true` path with the end of the `if` block.
           */
          context.trueForkContext.clear();
          context.trueForkContext.add(headSegments);
        } else {
          /*
           * The head segments are the path of the `else` block here.
           * Updates the `false` path with the end of the `else`
           * block.
           */
          context.falseForkContext.clear();
          context.falseForkContext.add(headSegments);
        }

        break;

      case 'loop':
        /*
         * Loops are addressed in popLoopContext().
         * This is called from popLoopContext().
         */
        return context;

      /* c8 ignore next */
      default:
        throw new Error('unreachable');
    }

    // Merges all paths.
    const prevForkContext = context.trueForkContext;

    prevForkContext.addAll(context.falseForkContext);
    forkContext.replaceHead(prevForkContext.makeNext(0, -1));

    return context;
  }

  /**
   * Makes a code path segment of the right-hand operand of a logical
   * expression.
   * @throws {Error} (Unreachable.)
   * @returns {void}
   */
  makeLogicalRight() {
    const context = this.choiceContext;
    const forkContext = this.forkContext;

    if (context.processed) {
      /*
       * This got segments already from the child choice context.
       * Creates the next path from own true/false fork context.
       */
      let prevForkContext;

      switch (context.kind) {
        case '&&': // if true then go to the right-hand side.
          prevForkContext = context.trueForkContext;
          break;
        case '||': // if false then go to the right-hand side.
          prevForkContext = context.falseForkContext;
          break;
        case '??': // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext.
          prevForkContext = context.qqForkContext;
          break;
        default:
          throw new Error('unreachable');
      }

      forkContext.replaceHead(prevForkContext.makeNext(0, -1));
      prevForkContext.clear();
      context.processed = false;
    } else {
      /*
       * This did not get segments from the child choice context.
       * So addresses the head segments.
       * The head segments are the path of the left-hand operand.
       */
      switch (context.kind) {
        case '&&': // the false path can short-circuit.
          context.falseForkContext.add(forkContext.head);
          break;
        case '||': // the true path can short-circuit.
          context.trueForkContext.add(forkContext.head);
          break;
        case '??': // both can short-circuit.
          context.trueForkContext.add(forkContext.head);
          context.falseForkContext.add(forkContext.head);
          break;
        default:
          throw new Error('unreachable');
      }

      forkContext.replaceHead(forkContext.makeNext(-1, -1));
    }
  }

  /**
   * Makes a code path segment of the `if` block.
   * @returns {void}
   */
  makeIfConsequent() {
    const context = this.choiceContext;
    const forkContext = this.forkContext;

    /*
     * If any result were not transferred from child contexts,
     * this sets the head segments to both cases.
     * The head segments are the path of the test expression.
     */
    if (!context.processed) {
      context.trueForkContext.add(forkContext.head);
      context.falseForkContext.add(forkContext.head);
      context.qqForkContext.add(forkContext.head);
    }

    context.processed = false;

    // Creates new path from the `true` case.
    forkContext.replaceHead(context.trueForkContext.makeNext(0, -1));
  }

  /**
   * Makes a code path segment of the `else` block.
   * @returns {void}
   */
  makeIfAlternate() {
    const context = this.choiceContext;
    const forkContext = this.forkContext;

    /*
     * The head segments are the path of the `if` block.
     * Updates the `true` path with the end of the `if` block.
     */
    context.trueForkContext.clear();
    context.trueForkContext.add(forkContext.head);
    context.processed = true;

    // Creates new path from the `false` case.
    forkContext.replaceHead(context.falseForkContext.makeNext(0, -1));
  }

  //--------------------------------------------------------------------------
  // ChainExpression
  //--------------------------------------------------------------------------

  /**
   * Push a new `ChainExpression` context to the stack.
   * This method is called on entering to each `ChainExpression` node.
   * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node.
   * @returns {void}
   */
  pushChainContext() {
    this.chainContext = {
      upper: this.chainContext,
      countChoiceContexts: 0,
    };
  }

  /**
   * Pop a `ChainExpression` context from the stack.
   * This method is called on exiting from each `ChainExpression` node.
   * This merges all forks of the last optional chaining.
   * @returns {void}
   */
  popChainContext() {
    const context = this.chainContext;

    this.chainContext = context.upper;

    // pop all choice contexts of this.
    for (let i = context.countChoiceContexts; i > 0; --i) {
      this.popChoiceContext();
    }
  }

  /**
   * Create a choice context for optional access.
   * This method is called on entering to each `(Call|Member)Expression[optional=true]` node.
   * This creates a choice context as similar to `LogicalExpression[operator="??"]` node.
   * @returns {void}
   */
  makeOptionalNode() {
    if (this.chainContext) {
      this.chainContext.countChoiceContexts += 1;
      this.pushChoiceContext('??', false);
    }
  }

  /**
   * Create a fork.
   * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node.
   * @returns {void}
   */
  makeOptionalRight() {
    if (this.chainContext) {
      this.makeLogicalRight();
    }
  }

  //--------------------------------------------------------------------------
  // SwitchStatement
  //--------------------------------------------------------------------------

  /**
   * Creates a context object of SwitchStatement and stacks it.
   * @param {boolean} hasCase `true` if the switch statement has one or more
   *   case parts.
   * @param {string|null} label The label text.
   * @returns {void}
   */
  pushSwitchContext(hasCase, label) {
    this.switchContext = {
      upper: this.switchContext,
      hasCase,
      defaultSegments: null,
      defaultBodySegments: null,
      foundDefault: false,
      lastIsDefault: false,
      countForks: 0,
    };

    this.pushBreakContext(true, label);
  }

  /**
   * Pops the last context of SwitchStatement and finalizes it.
   *
   * - Disposes all forking stack for `case` and `default`.
   * - Creates the next code path segment from `context.brokenForkContext`.
   * - If the last `SwitchCase` node is not a `default` part, creates a path
   *   to the `default` body.
   * @returns {void}
   */
  popSwitchContext() {
    const context = this.switchContext;

    this.switchContext = context.upper;

    const forkContext = this.forkContext;
    const brokenForkContext = this.popBreakContext().brokenForkContext;

    if (context.countForks === 0) {
      /*
       * When there is only one `default` chunk and there is one or more
       * `break` statements, even if forks are nothing, it needs to merge
       * those.
       */
      if (!brokenForkContext.empty) {
        brokenForkContext.add(forkContext.makeNext(-1, -1));
        forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
      }

      return;
    }

    const lastSegments = forkContext.head;

    this.forkBypassPath();
    const lastCaseSegments = forkContext.head;

    /*
     * `brokenForkContext` is used to make the next segment.
     * It must add the last segment into `brokenForkContext`.
     */
    brokenForkContext.add(lastSegments);

    /*
     * A path which is failed in all case test should be connected to path
     * of `default` chunk.
     */
    if (!context.lastIsDefault) {
      if (context.defaultBodySegments) {
        /*
         * Remove a link from `default` label to its chunk.
         * It's false route.
         */
        removeConnection(context.defaultSegments, context.defaultBodySegments);
        makeLooped(this, lastCaseSegments, context.defaultBodySegments);
      } else {
        /*
         * It handles the last case body as broken if `default` chunk
         * does not exist.
         */
        brokenForkContext.add(lastCaseSegments);
      }
    }

    // Pops the segment context stack until the entry segment.
    for (let i = 0; i < context.countForks; ++i) {
      this.forkContext = this.forkContext.upper;
    }

    /*
     * Creates a path from all brokenForkContext paths.
     * This is a path after switch statement.
     */
    this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
  }

  /**
   * Makes a code path segment for a `SwitchCase` node.
   * @param {boolean} isEmpty `true` if the body is empty.
   * @param {boolean} isDefault `true` if the body is the default case.
   * @returns {void}
   */
  makeSwitchCaseBody(isEmpty, isDefault) {
    const context = this.switchContext;

    if (!context.hasCase) {
      return;
    }

    /*
     * Merge forks.
     * The parent fork context has two segments.
     * Those are from the current case and the body of the previous case.
     */
    const parentForkContext = this.forkContext;
    const forkContext = this.pushForkContext();

    forkContext.add(parentForkContext.makeNext(0, -1));

    /*
     * Save `default` chunk info.
     * If the `default` label is not at the last, we must make a path from
     * the last `case` to the `default` chunk.
     */
    if (isDefault) {
      context.defaultSegments = parentForkContext.head;
      if (isEmpty) {
        context.foundDefault = true;
      } else {
        context.defaultBodySegments = forkContext.head;
      }
    } else {
      if (!isEmpty && context.foundDefault) {
        context.foundDefault = false;
        context.defaultBodySegments = forkContext.head;
      }
    }

    context.lastIsDefault = isDefault;
    context.countForks += 1;
  }

  //--------------------------------------------------------------------------
  // TryStatement
  //--------------------------------------------------------------------------

  /**
   * Creates a context object of TryStatement and stacks it.
   * @param {boolean} hasFinalizer `true` if the try statement has a
   *   `finally` block.
   * @returns {void}
   */
  pushTryContext(hasFinalizer) {
    this.tryContext = {
      upper: this.tryContext,
      position: 'try',
      hasFinalizer,

      returnedForkContext: hasFinalizer
        ? ForkContext.newEmpty(this.forkContext)
        : null,

      thrownForkContext: ForkContext.newEmpty(this.forkContext),
      lastOfTryIsReachable: false,
      lastOfCatchIsReachable: false,
    };
  }

  /**
   * Pops the last context of TryStatement and finalizes it.
   * @returns {void}
   */
  popTryContext() {
    const context = this.tryContext;

    this.tryContext = context.upper;

    if (context.position === 'catch') {
      // Merges two paths from the `try` block and `catch` block merely.
      this.popForkContext();
      return;
    }

    /*
     * The following process is executed only when there is the `finally`
     * block.
     */

    const returned = context.returnedForkContext;
    const thrown = context.thrownForkContext;

    if (returned.empty && thrown.empty) {
      return;
    }

    // Separate head to normal paths and leaving paths.
    const headSegments = this.forkContext.head;

    this.forkContext = this.forkContext.upper;
    const normalSegments = headSegments.slice(0, (headSegments.length / 2) | 0);
    const leavingSegments = headSegments.slice((headSegments.length / 2) | 0);

    // Forwards the leaving path to upper contexts.
    if (!returned.empty) {
      getReturnContext(this).returnedForkContext.add(leavingSegments);
    }
    if (!thrown.empty) {
      getThrowContext(this).thrownForkContext.add(leavingSegments);
    }

    // Sets the normal path as the next.
    this.forkContext.replaceHead(normalSegments);

    /*
     * If both paths of the `try` block and the `catch` block are
     * unreachable, the next path becomes unreachable as well.
     */
    if (!context.lastOfTryIsReachable && !context.lastOfCatchIsReachable) {
      this.forkContext.makeUnreachable();
    }
  }

  /**
   * Makes a code path segment for a `catch` block.
   * @returns {void}
   */
  makeCatchBlock() {
    const context = this.tryContext;
    const forkContext = this.forkContext;
    const thrown = context.thrownForkContext;

    // Update state.
    context.position = 'catch';
    context.thrownForkContext = ForkContext.newEmpty(forkContext);
    context.lastOfTryIsReachable = forkContext.reachable;

    // Merge thrown paths.
    thrown.add(forkContext.head);
    const thrownSegments = thrown.makeNext(0, -1);

    // Fork to a bypass and the merged thrown path.
    this.pushForkContext();
    this.forkBypassPath();
    this.forkContext.add(thrownSegments);
  }

  /**
   * Makes a code path segment for a `finally` block.
   *
   * In the `finally` block, parallel paths are created. The parallel paths
   * are used as leaving-paths. The leaving-paths are paths from `return`
   * statements and `throw` statements in a `try` block or a `catch` block.
   * @returns {void}
   */
  makeFinallyBlock() {
    const context = this.tryContext;
    let forkContext = this.forkContext;
    const returned = context.returnedForkContext;
    const thrown = context.thrownForkContext;
    const headOfLeavingSegments = forkContext.head;

    // Update state.
    if (context.position === 'catch') {
      // Merges two paths from the `try` block and `catch` block.
      this.popForkContext();
      forkContext = this.forkContext;

      context.lastOfCatchIsReachable = forkContext.reachable;
    } else {
      context.lastOfTryIsReachable = forkContext.reachable;
    }
    context.position = 'finally';

    if (returned.empty && thrown.empty) {
      // This path does not leave.
      return;
    }

    /*
     * Create a parallel segment from merging returned and thrown.
     * This segment will leave at the end of this finally block.
     */
    const segments = forkContext.makeNext(-1, -1);

    for (let i = 0; i < forkContext.count; ++i) {
      const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]];

      for (let j = 0; j < returned.segmentsList.length; ++j) {
        prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]);
      }
      for (let j = 0; j < thrown.segmentsList.length; ++j) {
        prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]);
      }

      segments.push(
        CodePathSegment.newNext(
          this.idGenerator.next(),
          prevSegsOfLeavingSegment,
        ),
      );
    }

    this.pushForkContext(true);
    this.forkContext.add(segments);
  }

  /**
   * Makes a code path segment from the first throwable node to the `catch`
   * block or the `finally` block.
   * @returns {void}
   */
  makeFirstThrowablePathInTryBlock() {
    const forkContext = this.forkContext;

    if (!forkContext.reachable) {
      return;
    }

    const context = getThrowContext(this);

    if (
      context === this ||
      context.position !== 'try' ||
      !context.thrownForkContext.empty
    ) {
      return;
    }

    context.thrownForkContext.add(forkContext.head);
    forkContext.replaceHead(forkContext.makeNext(-1, -1));
  }

  //--------------------------------------------------------------------------
  // Loop Statements
  //--------------------------------------------------------------------------

  /**
   * Creates a context object of a loop statement and stacks it.
   * @param {string} type The type of the node which was triggered. One of
   *   `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`,
   *   and `ForStatement`.
   * @param {string|null} label A label of the node which was triggered.
   * @throws {Error} (Unreachable - unknown type.)
   * @returns {void}
   */
  pushLoopContext(type, label) {
    const forkContext = this.forkContext;
    const breakContext = this.pushBreakContext(true, label);

    switch (type) {
      case 'WhileStatement':
        this.pushChoiceContext('loop', false);
        this.loopContext = {
          upper: this.loopContext,
          type,
          label,
          test: void 0,
          continueDestSegments: null,
          brokenForkContext: breakContext.brokenForkContext,
        };
        break;

      case 'DoWhileStatement':
        this.pushChoiceContext('loop', false);
        this.loopContext = {
          upper: this.loopContext,
          type,
          label,
          test: void 0,
          entrySegments: null,
          continueForkContext: ForkContext.newEmpty(forkContext),
          brokenForkContext: breakContext.brokenForkContext,
        };
        break;

      case 'ForStatement':
        this.pushChoiceContext('loop', false);
        this.loopContext = {
          upper: this.loopContext,
          type,
          label,
          test: void 0,
          endOfInitSegments: null,
          testSegments: null,
          endOfTestSegments: null,
          updateSegments: null,
          endOfUpdateSegments: null,
          continueDestSegments: null,
          brokenForkContext: breakContext.brokenForkContext,
        };
        break;

      case 'ForInStatement':
      case 'ForOfStatement':
        this.loopContext = {
          upper: this.loopContext,
          type,
          label,
          prevSegments: null,
          leftSegments: null,
          endOfLeftSegments: null,
          continueDestSegments: null,
          brokenForkContext: breakContext.brokenForkContext,
        };
        break;

      /* c8 ignore next */
      default:
        throw new Error(`unknown type: "${type}"`);
    }
  }

  /**
   * Pops the last context of a loop statement and finalizes it.
   * @throws {Error} (Unreachable - unknown type.)
   * @returns {void}
   */
  popLoopContext() {
    const context = this.loopContext;

    this.loopContext = context.upper;

    const forkContext = this.forkContext;
    const brokenForkContext = this.popBreakContext().brokenForkContext;

    // Creates a looped path.
    switch (context.type) {
      case 'WhileStatement':
      case 'ForStatement':
        this.popChoiceContext();
        makeLooped(this, forkContext.head, context.continueDestSegments);
        break;

      case 'DoWhileStatement': {
        const choiceContext = this.popChoiceContext();

        if (!choiceContext.processed) {
          choiceContext.trueForkContext.add(forkContext.head);
          choiceContext.falseForkContext.add(forkContext.head);
        }
        if (context.test !== true) {
          brokenForkContext.addAll(choiceContext.falseForkContext);
        }

        // `true` paths go to looping.
        const segmentsList = choiceContext.trueForkContext.segmentsList;

        for (let i = 0; i < segmentsList.length; ++i) {
          makeLooped(this, segmentsList[i], context.entrySegments);
        }
        break;
      }

      case 'ForInStatement':
      case 'ForOfStatement':
        brokenForkContext.add(forkContext.head);
        makeLooped(this, forkContext.head, context.leftSegments);
        break;

      /* c8 ignore next */
      default:
        throw new Error('unreachable');
    }

    // Go next.
    if (brokenForkContext.empty) {
      forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
    } else {
      forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
    }
  }

  /**
   * Makes a code path segment for the test part of a WhileStatement.
   * @param {boolean|undefined} test The test value (only when constant).
   * @returns {void}
   */
  makeWhileTest(test) {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const testSegments = forkContext.makeNext(0, -1);

    // Update state.
    context.test = test;
    context.continueDestSegments = testSegments;
    forkContext.replaceHead(testSegments);
  }

  /**
   * Makes a code path segment for the body part of a WhileStatement.
   * @returns {void}
   */
  makeWhileBody() {
    const context = this.loopContext;
    const choiceContext = this.choiceContext;
    const forkContext = this.forkContext;

    if (!choiceContext.processed) {
      choiceContext.trueForkContext.add(forkContext.head);
      choiceContext.falseForkContext.add(forkContext.head);
    }

    // Update state.
    if (context.test !== true) {
      context.brokenForkContext.addAll(choiceContext.falseForkContext);
    }
    forkContext.replaceHead(choiceContext.trueForkContext.makeNext(0, -1));
  }

  /**
   * Makes a code path segment for the body part of a DoWhileStatement.
   * @returns {void}
   */
  makeDoWhileBody() {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const bodySegments = forkContext.makeNext(-1, -1);

    // Update state.
    context.entrySegments = bodySegments;
    forkContext.replaceHead(bodySegments);
  }

  /**
   * Makes a code path segment for the test part of a DoWhileStatement.
   * @param {boolean|undefined} test The test value (only when constant).
   * @returns {void}
   */
  makeDoWhileTest(test) {
    const context = this.loopContext;
    const forkContext = this.forkContext;

    context.test = test;

    // Creates paths of `continue` statements.
    if (!context.continueForkContext.empty) {
      context.continueForkContext.add(forkContext.head);
      const testSegments = context.continueForkContext.makeNext(0, -1);

      forkContext.replaceHead(testSegments);
    }
  }

  /**
   * Makes a code path segment for the test part of a ForStatement.
   * @param {boolean|undefined} test The test value (only when constant).
   * @returns {void}
   */
  makeForTest(test) {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const endOfInitSegments = forkContext.head;
    const testSegments = forkContext.makeNext(-1, -1);

    // Update state.
    context.test = test;
    context.endOfInitSegments = endOfInitSegments;
    context.continueDestSegments = context.testSegments = testSegments;
    forkContext.replaceHead(testSegments);
  }

  /**
   * Makes a code path segment for the update part of a ForStatement.
   * @returns {void}
   */
  makeForUpdate() {
    const context = this.loopContext;
    const choiceContext = this.choiceContext;
    const forkContext = this.forkContext;

    // Make the next paths of the test.
    if (context.testSegments) {
      finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
    } else {
      context.endOfInitSegments = forkContext.head;
    }

    // Update state.
    const updateSegments = forkContext.makeDisconnected(-1, -1);

    context.continueDestSegments = context.updateSegments = updateSegments;
    forkContext.replaceHead(updateSegments);
  }

  /**
   * Makes a code path segment for the body part of a ForStatement.
   * @returns {void}
   */
  makeForBody() {
    const context = this.loopContext;
    const choiceContext = this.choiceContext;
    const forkContext = this.forkContext;

    // Update state.
    if (context.updateSegments) {
      context.endOfUpdateSegments = forkContext.head;

      // `update` -> `test`
      if (context.testSegments) {
        makeLooped(this, context.endOfUpdateSegments, context.testSegments);
      }
    } else if (context.testSegments) {
      finalizeTestSegmentsOfFor(context, choiceContext, forkContext.head);
    } else {
      context.endOfInitSegments = forkContext.head;
    }

    let bodySegments = context.endOfTestSegments;

    if (!bodySegments) {
      /*
       * If there is not the `test` part, the `body` path comes from the
       * `init` part and the `update` part.
       */
      const prevForkContext = ForkContext.newEmpty(forkContext);

      prevForkContext.add(context.endOfInitSegments);
      if (context.endOfUpdateSegments) {
        prevForkContext.add(context.endOfUpdateSegments);
      }

      bodySegments = prevForkContext.makeNext(0, -1);
    }
    context.continueDestSegments = context.continueDestSegments || bodySegments;
    forkContext.replaceHead(bodySegments);
  }

  /**
   * Makes a code path segment for the left part of a ForInStatement and a
   * ForOfStatement.
   * @returns {void}
   */
  makeForInOfLeft() {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const leftSegments = forkContext.makeDisconnected(-1, -1);

    // Update state.
    context.prevSegments = forkContext.head;
    context.leftSegments = context.continueDestSegments = leftSegments;
    forkContext.replaceHead(leftSegments);
  }

  /**
   * Makes a code path segment for the right part of a ForInStatement and a
   * ForOfStatement.
   * @returns {void}
   */
  makeForInOfRight() {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const temp = ForkContext.newEmpty(forkContext);

    temp.add(context.prevSegments);
    const rightSegments = temp.makeNext(-1, -1);

    // Update state.
    context.endOfLeftSegments = forkContext.head;
    forkContext.replaceHead(rightSegments);
  }

  /**
   * Makes a code path segment for the body part of a ForInStatement and a
   * ForOfStatement.
   * @returns {void}
   */
  makeForInOfBody() {
    const context = this.loopContext;
    const forkContext = this.forkContext;
    const temp = ForkContext.newEmpty(forkContext);

    temp.add(context.endOfLeftSegments);
    const bodySegments = temp.makeNext(-1, -1);

    // Make a path: `right` -> `left`.
    makeLooped(this, forkContext.head, context.leftSegments);

    // Update state.
    context.brokenForkContext.add(forkContext.head);
    forkContext.replaceHead(bodySegments);
  }

  //--------------------------------------------------------------------------
  // Control Statements
  //--------------------------------------------------------------------------

  /**
   * Creates new context for BreakStatement.
   * @param {boolean} breakable The flag to indicate it can break by
   *      an unlabeled BreakStatement.
   * @param {string|null} label The label of this context.
   * @returns {Object} The new context.
   */
  pushBreakContext(breakable, label) {
    this.breakContext = {
      upper: this.breakContext,
      breakable,
      label,
      brokenForkContext: ForkContext.newEmpty(this.forkContext),
    };
    return this.breakContext;
  }

  /**
   * Removes the top item of the break context stack.
   * @returns {Object} The removed context.
   */
  popBreakContext() {
    const context = this.breakContext;
    const forkContext = this.forkContext;

    this.breakContext = context.upper;

    // Process this context here for other than switches and loops.
    if (!context.breakable) {
      const brokenForkContext = context.brokenForkContext;

      if (!brokenForkContext.empty) {
        brokenForkContext.add(forkContext.head);
        forkContext.replaceHead(brokenForkContext.makeNext(0, -1));
      }
    }

    return context;
  }

  /**
   * Makes a path for a `break` statement.
   *
   * It registers the head segment to a context of `break`.
   * It makes new unreachable segment, then it set the head with the segment.
   * @param {string} label A label of the break statement.
   * @returns {void}
   */
  makeBreak(label) {
    const forkContext = this.forkContext;

    if (!forkContext.reachable) {
      return;
    }

    const context = getBreakContext(this, label);

    if (context) {
      context.brokenForkContext.add(forkContext.head);
    }

    /* c8 ignore next */
    forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
  }

  /**
   * Makes a path for a `continue` statement.
   *
   * It makes a looping path.
   * It makes new unreachable segment, then it set the head with the segment.
   * @param {string} label A label of the continue statement.
   * @returns {void}
   */
  makeContinue(label) {
    const forkContext = this.forkContext;

    if (!forkContext.reachable) {
      return;
    }

    const context = getContinueContext(this, label);

    if (context) {
      if (context.continueDestSegments) {
        makeLooped(this, forkContext.head, context.continueDestSegments);

        // If the context is a for-in/of loop, this effects a break also.
        if (
          context.type === 'ForInStatement' ||
          context.type === 'ForOfStatement'
        ) {
          context.brokenForkContext.add(forkContext.head);
        }
      } else {
        context.continueForkContext.add(forkContext.head);
      }
    }
    forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
  }

  /**
   * Makes a path for a `return` statement.
   *
   * It registers the head segment to a context of `return`.
   * It makes new unreachable segment, then it set the head with the segment.
   * @returns {void}
   */
  makeReturn() {
    const forkContext = this.forkContext;

    if (forkContext.reachable) {
      getReturnContext(this).returnedForkContext.add(forkContext.head);
      forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
    }
  }

  /**
   * Makes a path for a `throw` statement.
   *
   * It registers the head segment to a context of `throw`.
   * It makes new unreachable segment, then it set the head with the segment.
   * @returns {void}
   */
  makeThrow() {
    const forkContext = this.forkContext;

    if (forkContext.reachable) {
      getThrowContext(this).thrownForkContext.add(forkContext.head);
      forkContext.replaceHead(forkContext.makeUnreachable(-1, -1));
    }
  }

  /**
   * Makes the final path.
   * @returns {void}
   */
  makeFinal() {
    const segments = this.currentSegments;

    if (segments.length > 0 && segments[0].reachable) {
      this.returnedForkContext.add(segments);
    }
  }
}

module.exports = CodePathState;