import {
  BlockId,
  ReactiveFunction,
  ReactiveScopeBlock,
  ReactiveTerminalStatement,
  makeBlockId,
} from '../HIR';
import {getOrInsertDefault} from '../Utils/utils';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';

export function stabilizeBlockIds(fn: ReactiveFunction): void {
  const referenced: Set<BlockId> = new Set();
  visitReactiveFunction(fn, new CollectReferencedLabels(), referenced);

  const mappings = new Map<BlockId, BlockId>();
  for (const blockId of referenced) {
    mappings.set(blockId, makeBlockId(mappings.size));
  }

  visitReactiveFunction(fn, new RewriteBlockIds(), mappings);
}

class CollectReferencedLabels extends ReactiveFunctionVisitor<Set<BlockId>> {
  override visitScope(scope: ReactiveScopeBlock, state: Set<BlockId>): void {
    const {earlyReturnValue} = scope.scope;
    if (earlyReturnValue != null) {
      state.add(earlyReturnValue.label);
    }
    this.traverseScope(scope, state);
  }
  override visitTerminal(
    stmt: ReactiveTerminalStatement,
    state: Set<BlockId>,
  ): void {
    if (stmt.label != null) {
      if (!stmt.label.implicit) {
        state.add(stmt.label.id);
      }
    }
    this.traverseTerminal(stmt, state);
  }
}

class RewriteBlockIds extends ReactiveFunctionVisitor<Map<BlockId, BlockId>> {
  override visitScope(
    scope: ReactiveScopeBlock,
    state: Map<BlockId, BlockId>,
  ): void {
    const {earlyReturnValue} = scope.scope;
    if (earlyReturnValue != null) {
      const rewrittenId = getOrInsertDefault(
        state,
        earlyReturnValue.label,
        state.size,
      );
      earlyReturnValue.label = makeBlockId(rewrittenId);
    }
    this.traverseScope(scope, state);
  }
  override visitTerminal(
    stmt: ReactiveTerminalStatement,
    state: Map<BlockId, BlockId>,
  ): void {
    if (stmt.label != null) {
      const rewrittenId = getOrInsertDefault(state, stmt.label.id, state.size);
      stmt.label.id = makeBlockId(rewrittenId);
    }

    const terminal = stmt.terminal;
    if (terminal.kind === 'break' || terminal.kind === 'continue') {
      const rewrittenId = getOrInsertDefault(
        state,
        terminal.target,
        state.size,
      );
      terminal.target = makeBlockId(rewrittenId);
    }
    this.traverseTerminal(stmt, state);
  }
}
