/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package consulo.ide.impl.idea.codeInsight.controlflow;

import consulo.ide.impl.idea.codeInsight.controlflow.impl.ConditionalInstructionImpl;
import consulo.ide.impl.idea.codeInsight.controlflow.impl.ControlFlowImpl;
import consulo.ide.impl.idea.codeInsight.controlflow.impl.InstructionImpl;
import consulo.util.lang.Pair;
import consulo.language.psi.PsiElement;
import consulo.language.psi.PsiElementVisitor;
import consulo.language.psi.util.PsiTreeUtil;
import consulo.ide.impl.idea.util.containers.ContainerUtil;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

/**
* @author oleg
*/
public class ControlFlowBuilder {
  // Here we store all the instructions
  public List<Instruction> instructions;

  // The last instruction
  public Instruction prevInstruction;

  // Here we store all the pending instructions with their scope
  public List<Pair<PsiElement, Instruction>> pending;

  // Number of instructions already added
  public int instructionCount;

  public ControlFlowBuilder() {
    instructions = ContainerUtil.newArrayList();
    pending = ContainerUtil.newArrayList();
    instructionCount = 0;
  }

  @Nullable
  public Instruction findInstructionByElement(final PsiElement element) {
    for (int i = instructions.size() - 1; i >= 0; i--) {
      final Instruction instruction = instructions.get(i);
      if (element.equals(instruction.getElement())) {
        return instruction;
      }
    }
    return null;
  }

  /**
   * Adds edge between 2 edges
   *
   * @param beginInstruction Begin of new edge
   * @param endInstruction   End of new edge
   */
  public void addEdge(final Instruction beginInstruction, final Instruction endInstruction) {
    if (beginInstruction == null || endInstruction == null) {
      return;
    }
    if (!beginInstruction.allSucc().contains(endInstruction)) {
      beginInstruction.allSucc().add(endInstruction);
    }

    if (!endInstruction.allPred().contains(beginInstruction)) {
      endInstruction.allPred().add(beginInstruction);
    }
  }

  /**
   * Add new node and set prev instruction pointing to this instruction
   *
   * @param instruction new instruction
   */
  public void addNode(final Instruction instruction) {
    instructions.add(instruction);
    if (prevInstruction != null) {
      addEdge(prevInstruction, instruction);
    }
    prevInstruction = instruction;
  }

  /**
   * Stops control flow, used for break, next, redo
   */
  public void flowAbrupted() {
    prevInstruction = null;
  }

  /**
   * Adds pending edge in pendingScope
   *
   * @param pendingScope Scope for instruction
   * @param instruction  "Last" pending instruction
   */
  public void addPendingEdge(@Nullable final PsiElement pendingScope, final Instruction instruction) {
    if (instruction == null) {
      return;
    }

    int i = 0;
    // another optimization! Place pending before first scope, not contained in pendingScope
    // the same logic is used in checkPending
    if (pendingScope != null) {
      for (; i < pending.size(); i++) {
        final Pair<PsiElement, Instruction> pair = pending.get(i);
        final PsiElement scope = pair.getFirst();
        if (scope == null) {
          continue;
        }
        if (!PsiTreeUtil.isAncestor(scope, pendingScope, true)) {
          break;
        }
      }
    }
    pending.add(i, Pair.create(pendingScope, instruction));
  }

  /**
   * Creates edges from the pending list to the specified instruction.
   *
   * @param instruction target instruction for pending edges
   */
  public void checkPending(@Nonnull final Instruction instruction) {
    final PsiElement element = instruction.getElement();
    if (element == null) {
      // if element is null (fake element, we just process all pending)
      for (Pair<PsiElement, Instruction> pair : pending) {
        addEdge(pair.getSecond(), instruction);
      }
      pending.clear();
    }
    else {
      // else we just process all the pending with scope containing in element
      // reverse order is just an optimization
      for (int i = pending.size() - 1; i >= 0; i--) {
        final Pair<PsiElement, Instruction> pair = pending.get(i);
        final PsiElement scopeWhenToAdd = pair.getFirst();
        if (scopeWhenToAdd == null) {
          continue;
        }
        if (!PsiTreeUtil.isAncestor(scopeWhenToAdd, element, false)) {
          addEdge(pair.getSecond(), instruction);
          pending.remove(i);
        }
        else {
          break;
        }
      }
    }
  }

  /**
   * Creates instruction for given element, and adds it to myInstructionsStack
   *
   * @param element Element to create instruction for
   * @return new instruction
   */
  public Instruction startNode(@Nullable final PsiElement element) {
    final Instruction instruction = new InstructionImpl(this, element);
    addNode(instruction);
    checkPending(instruction);
    return instruction;
  }

  /**
   * Creates conditional instruction for given element, and adds it to myInstructionsStack
   *
   * @param element Element to create instruction for
   * @return new instruction
   */
  public Instruction startConditionalNode(final PsiElement element, final PsiElement condition, final boolean result) {
    final ConditionalInstruction instruction = new ConditionalInstructionImpl(this, element, condition, result);
    addNode(instruction);
    checkPending(instruction);
    return instruction;
  }

  public ControlFlow build(final PsiElementVisitor visitor, final PsiElement element) {
    // create start pseudo node
    startNode(null);

    element.acceptChildren(visitor);

    // create end pseudo node and close all pending edges
    checkPending(startNode(null));

    final List<Instruction> result = instructions;
    return new ControlFlowImpl(result.toArray(new Instruction[result.size()]));
  }


  public interface PendingProcessor {
    void process(PsiElement pendingScope, Instruction instruction);
  }

  public void processPending(final PendingProcessor processor) {
    final List<Pair<PsiElement, Instruction>> pending = this.pending;
    this.pending = new ArrayList<Pair<PsiElement, Instruction>>();
    for (Pair<PsiElement, Instruction> pair : pending) {
      processor.process(pair.getFirst(), pair.getSecond());
    }
  }
}
