package jetbrains.mps.ide.dataFlow.presentation;

/*Generated by MPS */

import java.awt.Component;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.awt.Graphics;
import java.util.Collections;
import java.awt.event.MouseEvent;

public class ControlFlowGraph<T extends IInstruction<T>> {
  private static final int MARGIN_X = 20;
  private static final int MARGIN_Y = 20;
  private static final int LINE_SEGMENT_SIZE = 40;
  protected final IGraphCreator<T> myGraphCreator;
  protected final IProgram<T> myProgram;
  protected Component myComponent;
  protected List<IBlock> myBlocks = new ArrayList<IBlock>();
  protected List<Line> myLines = new ArrayList<Line>();
  protected Set<ArrowHead> myArrowHeads = new HashSet<ArrowHead>();
  private Map<Integer, FreeZone> myFreeZoneMap = new HashMap<Integer, FreeZone>();
  /*package*/ int myMaxLineIndentRight;
  /*package*/ int myMaxLineIndentLeft;
  private int myWidth;
  private int myHeight;
  public ControlFlowGraph(IProgram<T> program, IGraphCreator<T> graphCreator) {
    this.myProgram = program;
    this.myGraphCreator = graphCreator;
    ControlFlowGraph.this.buildBlocks();
  }
  public void buildBlocks() {
    Map<IInstruction<T>, IBlock> instructionToBlock = new HashMap<IInstruction<T>, IBlock>();
    for (IInstruction<T> instruction : this.myProgram.getInstructions()) {
      IBlock block = this.myGraphCreator.createBlock((T) instruction, MARGIN_X, 0, 0, 0);
      this.myBlocks.add(block);
      instructionToBlock.put(instruction, block);
    }
    for (IInstruction<T> instruction : this.myProgram.getInstructions()) {
      IBlock block = instructionToBlock.get(instruction);
      Set<IBlock> succ = new HashSet<IBlock>();
      for (IInstruction<T> instructionSucc : instruction.succ()) {
        succ.add(instructionToBlock.get(instructionSucc));
      }
      block.setSucc(succ);
    }
  }
  public void paint(Graphics g) {
    for (IBlock block : this.myBlocks) {
      block.paint(g);
    }
    List<Line> lines = new ArrayList<Line>(this.myLines);
    Collections.sort(lines);
    for (Line line : lines) {
      line.paint(g);
    }
    for (ArrowHead arrowHead : this.myArrowHeads) {
      arrowHead.paint(g);
    }
  }
  public void setComponent(Component component) {
    this.myComponent = component;
  }
  public void relayout() {
    this.myLines.clear();
    this.myArrowHeads.clear();
    this.myMaxLineIndentRight = 0;
    this.myMaxLineIndentLeft = 0;
    this.myFreeZoneMap = new HashMap<Integer, FreeZone>();
    for (IBlock block : this.myBlocks) {
      block.relayout(this.myComponent);
    }
    int maxWidth = 0;
    int maxHeight = 0;
    for (IBlock block : this.myBlocks) {
      maxWidth = Math.max(maxWidth, block.getWidth());
      maxHeight = Math.max(maxHeight, block.getHeight());
    }
    int y = MARGIN_Y;
    for (IBlock block : this.myBlocks) {
      block.setWidth(maxWidth);
      block.setX(MARGIN_X);
      block.setY(y);
      y += block.getHeight();
      y += maxHeight / 2;
    }
    for (int i = 0; i < this.myBlocks.size(); i++) {
      IBlock block = this.myBlocks.get(i);
      for (IBlock succBlock : block.succ()) {
        if (this.myBlocks.indexOf(succBlock) == i + 1) {
          ControlFlowGraph.this.addSimpleLine(block, succBlock);
        } else {
          ControlFlowGraph.this.addAdditionalLine(block, succBlock);
        }
      }
    }
    ControlFlowGraph.this.shiftLeft(this.myMaxLineIndentLeft * LINE_SEGMENT_SIZE);
    this.myWidth = MARGIN_X * 2 + maxWidth + (this.myMaxLineIndentRight + this.myMaxLineIndentLeft) * LINE_SEGMENT_SIZE;
    this.myHeight = y + MARGIN_Y;
  }
  private void shiftLeft(int indent) {
    for (IBlock block : this.myBlocks) {
      block.setX(block.getX() + indent);
    }
    for (ArrowHead arrowHead : this.myArrowHeads) {
      arrowHead.setX(arrowHead.getX() + indent);
    }
    for (Line line : this.myLines) {
      line.shiftLeft(indent);
    }
  }
  protected void addAdditionalLine(IBlock startBlock, IBlock endBlock) {
    addAdditionalLine(startBlock, endBlock, new SimpleLineCreator());
  }
  protected void addAdditionalLine(IBlock startBlock, IBlock endBlock, LineCreator lineCreator) {
    int startIndex = this.myBlocks.indexOf(startBlock);
    int endIndex = this.myBlocks.indexOf(endBlock);
    int rightIndent = 0;
    int leftIndent = 0;
    int first = Math.min(startIndex, endIndex);
    int last = Math.max(startIndex, endIndex);
    for (int i = 1; true; i++) {
      boolean canBeAdded = ControlFlowGraph.this.canBeAdded(first, last, i);
      if (canBeAdded) {
        rightIndent = i;
        break;
      }
      canBeAdded = ControlFlowGraph.this.canBeAdded(first, last, -i);
      if (canBeAdded) {
        leftIndent = i;
        break;
      }
    }
    this.myMaxLineIndentRight = Math.max(this.myMaxLineIndentRight, rightIndent);
    this.myMaxLineIndentLeft = Math.max(this.myMaxLineIndentLeft, leftIndent);
    int startBlockLevel = startBlock.getY() + startBlock.getHeight() / 2;
    int endBlockLevel = endBlock.getY() + endBlock.getHeight() / 2;
    if (rightIndent != 0) {
      int startBlockExit = startBlock.getX() + startBlock.getWidth();
      int endBlockExit = endBlock.getX() + endBlock.getWidth();
      this.myLines.add(lineCreator.createLine(startBlockExit, startBlockExit + LINE_SEGMENT_SIZE * rightIndent, startBlockLevel, LineDirection.HORIZONTAL));
      this.myLines.add(lineCreator.createLine(endBlockExit, endBlockExit + LINE_SEGMENT_SIZE * rightIndent, endBlockLevel, LineDirection.HORIZONTAL));
      this.myLines.add(lineCreator.createLine(startBlockLevel, endBlockLevel, startBlockExit + LINE_SEGMENT_SIZE * rightIndent, LineDirection.VERTICAL));
      this.myArrowHeads.add(createArrowHead(endBlockExit, endBlockLevel, ArrowHeadDirection.LEFT));
    } else
    if (leftIndent != 0) {
      int startBlockExit = startBlock.getX();
      int endBlockExit = endBlock.getX();
      this.myLines.add(lineCreator.createLine(startBlockExit, startBlockExit - LINE_SEGMENT_SIZE * leftIndent, startBlockLevel, LineDirection.HORIZONTAL));
      this.myLines.add(lineCreator.createLine(endBlockExit, endBlockExit - LINE_SEGMENT_SIZE * leftIndent, endBlockLevel, LineDirection.HORIZONTAL));
      this.myLines.add(lineCreator.createLine(startBlockLevel, endBlockLevel, startBlockExit - LINE_SEGMENT_SIZE * leftIndent, LineDirection.VERTICAL));
      this.myArrowHeads.add(createArrowHead(endBlockExit, endBlockLevel, ArrowHeadDirection.RIGHT));
    }
  }
  private boolean canBeAdded(int first, int last, int i) {
    FreeZone freeZone = this.myFreeZoneMap.get(i);
    if (freeZone == null) {
      freeZone = new FreeZone(0, this.myBlocks.size());
      this.myFreeZoneMap.put(i, freeZone);
    }
    boolean canBeAdded = freeZone.canBeAdded(first, last);
    return canBeAdded;
  }

  protected void addSimpleLine(IBlock block, IBlock nextBlock) {
    addSimpleLine(block, nextBlock, new SimpleLineCreator());
  }
  protected void addSimpleLine(IBlock block, IBlock nextBlock, LineCreator lineCreator) {
    int levelX = block.getX() + block.getWidth() / 2;
    int y1 = block.getY() + block.getHeight();
    int y2 = nextBlock.getY();
    this.myLines.add(lineCreator.createLine(y1, y2, levelX, LineDirection.VERTICAL));
    this.myArrowHeads.add(createArrowHead(levelX, y2, ArrowHeadDirection.DOWN));
  }
  protected ArrowHead createArrowHead(int x, int y, ArrowHeadDirection direction) {
    return new ArrowHead(x, y, direction);
  }
  public int getWidth() {
    return this.myWidth;
  }
  public int getHeight() {
    return this.myHeight;
  }
  public void addBlockListener(IBlockListener listener) {
    for (IBlock block : this.myBlocks) {
      block.addBlockListener(listener);
    }
  }
  public void removeBlockListener(IBlockListener listener) {
    for (IBlock block : this.myBlocks) {
      block.removeBlockListener(listener);
    }
  }
  public void processMousePressed(MouseEvent event) {
    for (IBlock block : this.myBlocks) {
      if (block.processMousePressed(event)) {
        return;
      }
    }
  }

  protected interface LineCreator {
    Line createLine(int first, int second, int level, LineDirection direction);
  }
  private class SimpleLineCreator implements LineCreator {
    public Line createLine(int first, int second, int level, LineDirection direction) {
      return new Line(first, second, level, direction);
    }
  }
}
