/*
 * Copyright 2003-2011 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 jetbrains.mps.nodeEditor.cellLayout;

import jetbrains.mps.editor.runtime.TextBuilderImpl;
import jetbrains.mps.editor.runtime.style.DefaultBaseLine;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.nodeEditor.cells.EditorCell_Basic;
import jetbrains.mps.nodeEditor.cells.EditorCell_Indent;
import jetbrains.mps.nodeEditor.cells.GeometryUtil;
import jetbrains.mps.openapi.editor.TextBuilder;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;

public class CellLayout_Indent extends AbstractCellLayout {
  static boolean isOnNewLine(EditorCell root, EditorCell cell) {
    for (EditorCell current = cell; current != root; current = current.getParent()) {
      if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_ON_NEW_LINE)) {
        return true;
      }
      if (current.getParent() == null || current.getParent().firstCell() != current) {
        return false;
      }
    }
    return false;
  }

  private static int getIndent(EditorCell root, EditorCell cell, boolean overflow) {
    int result = 0;

    if (overflow) {
      result += 2;
    }

    while (cell != root) {
      if (cell.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT)) {
        result++;
      }

      cell = cell.getParent();
    }

    return result;
  }

  public static boolean isNewLineAfter(EditorCell root, EditorCell cell) {
    for (EditorCell current = cell; current != root; current = current.getParent()) {
      if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_NEW_LINE)) {
        return true;
      }
      EditorCell_Collection parent = current.getParent();

      if (parent != null && parent.getStyle().get(StyleAttributes.INDENT_LAYOUT_CHILDREN_NEWLINE)) {
        return true;
      }
      if (parent == null || parent.lastCell() != current) {
        return false;
      }
    }
    return false;
  }

  @Override
  public int getAscent(EditorCell_Collection editorCells) {
    for (EditorCell cell : editorCells) {
      if (cell.getStyle().get(StyleAttributes.BASE_LINE_CELL)) {
        return cell.getY() - editorCells.getY() + cell.getAscent();
      }
    }

    DefaultBaseLine bL = editorCells.getStyle().get(StyleAttributes.DEFAULT_BASE_LINE);

    int result = 0;
    for (EditorCell cell : editorCells) {
      result = cell.getAscent();
      if (result > 0) {
        break;
      }
    }

    switch (bL) {
      case FIRST: // default behavior
        return result;
      case CENTER:
        return Math.max(result, editorCells.getHeight() / 2);
      case LAST:
        if (!editorCells.isEmpty()) {
          EditorCell lastCell = editorCells.lastCell();
          return lastCell.getY() - editorCells.getY() + lastCell.getAscent();
        }
    }

    return 0;
  }

  @Override
  public void move(EditorCell_Collection editorCells, int deltaX, int deltaY) {
    if (editorCells.getParent() != null && editorCells.getParent().getCellLayout() instanceof CellLayout_Indent || deltaX == 0) {
      return;
    }
    // Triggering re-layout process for top-level EditorCell_Collection with indent layout on move.
    // Necessary to recalculate cell wrapping on moving EditorCell_Collection with indent layout enclosed
    // inside another (non-indent layout) cell. Such move can be performed as a part of layout process for
    // top-level cell with indent layout.
    editorCells.requestRelayout();
  }

  @Override
  public void doLayout(EditorCell_Collection editorCells) {
    if (editorCells.getParent() != null && editorCells.getParent().getCellLayout() instanceof CellLayout_Indent) {
      return;
    }

    new CellLayouter(editorCells, getMaxWidth(editorCells), getIndentSize()).layout();
  }

  private int getMaxWidth(EditorCell_Collection editorCells) {
    if (editorCells.getStyle().isSpecified(StyleAttributes.MAX_WIDTH)) {
      return editorCells.getX() + editorCells.getStyle().get(StyleAttributes.MAX_WIDTH);
    }
    return editorCells.getRootParent().getX() + EditorSettings.getInstance().getVerticalBoundWidth();
  }

  private int getIndentSize() {
    EditorSettings settings = EditorSettings.getInstance();
    return settings.getSpacesWidth(settings.getIndentSize());
  }

  @Override
  public TextBuilder doLayoutText(Iterable<EditorCell> editorCells) {
    Set<EditorCell> editorCellsSet = new HashSet<EditorCell>();
    for (EditorCell editorCell : editorCells) {
      editorCellsSet.add(editorCell);
    }
    TextBuilder result = new TextBuilderImpl();
    Iterator<EditorCell> iterator = editorCells.iterator();
    if (iterator.hasNext()) {
      boolean newLineAfter = false;
      EditorCell_Collection rootCell = iterator.next().getParent();
      for (EditorCell current : getIndentLeafs(rootCell)) {
        EditorCell childCell = current;
        while (childCell.getParent() != rootCell) {
          childCell = childCell.getParent();
        }
        if (!editorCellsSet.contains(childCell)) {
          continue;
        }
        if (isOnNewLine(rootCell, current) || newLineAfter) {
          newLineAfter = false;
          result.appendToTheRight(new TextBuilderImpl("\n"), true);
          for (int i = 0; i < getIndent(rootCell, current, false); i++) {
            result.appendToTheRight(new TextBuilderImpl(EditorCell_Indent.getIndentText()), false);
          }
        }

        result.appendToTheRight(current.renderText(), PunctuationUtil.hasLeftGap(current));

        if (isNewLineAfter(rootCell, current)) {
          newLineAfter = true;
        }
      }
    }
    return result;
  }

  @Override
  public List<? extends EditorCell> getSelectionCells(EditorCell_Collection editorCells) {
    return getIndentLeafs(editorCells);
  }

  @Override
  public List<Rectangle> getSelectionBounds(EditorCell_Collection editorCells) {
    List<Rectangle> result = new ArrayList<Rectangle>();
    List<EditorCell> indentLeafs = getIndentLeafs(editorCells);
    for (EditorCell leaf : indentLeafs) {
      result.add(GeometryUtil.getBounds(leaf));
    }
    return result;
  }

  @Override
  public boolean canBeFolded() {
    return true;
  }

  private static List<EditorCell> getIndentLeafs(EditorCell_Collection current) {
    List<EditorCell> result = new ArrayList<EditorCell>();
    collectCells(current, result, null);
    return result;
  }

  private static void collectCells(
      EditorCell_Collection current,
      List<EditorCell> frontier,
      List<EditorCell_Collection> collections) {

    for (EditorCell child : current) {
      if (child instanceof EditorCell_Collection) {
        EditorCell_Collection collection = (EditorCell_Collection) child;
        if (isIndentCollection(collection)) {
          collectCells(collection, frontier, collections);
        } else {
          frontier.add(child);
        }
      } else {
        frontier.add(child);
      }

      if (collections != null) {
        collections.add(current);
      }
    }
  }

  private static boolean isIndentCollection(EditorCell_Collection collection) {
    return collection.getCellLayout() instanceof CellLayout_Indent && !collection.isEmpty();
  }

  private class CellLayouter {
    private EditorCell_Collection myCell;

    private final int myX;
    private final int myMaxWidth;
    private int myWidth;
    private int myHeight;

    private int myLineWidth;
    private int myLineAscent;
    private int myLineDescent;
    private int myTopInset;
    private int myBottomInset;
    private boolean myOverflow;
    private int myLineIndent = 0;
    private List<EditorCell> myLineContent = new ArrayList<EditorCell>();
    private List<Integer> myLineWrapIndent = new ArrayList<Integer>();
    private int myIndentSize;

    private Stack<Integer> myIndentStack = new Stack<Integer>();
    private Stack<Integer> myWrapStack = new Stack<Integer>();
    private int myCurrentIndent;
    private int myCurrentIndentAfterWrap;

    private CellLayouter(EditorCell_Collection cell, int maxWidth, int indentSize) {
      myCell = cell;
      myX = myCell.getX();

      myWidth = 0;
      myHeight = 0;

      myLineWidth = 0;
      myLineAscent = 0;
      myLineDescent = 0;
      myTopInset = 0;
      myBottomInset = 0;
      myCurrentIndent = 0;

      myMaxWidth = maxWidth;
      myIndentSize = indentSize;

      myCurrentIndentAfterWrap = myIndentSize * 2;
    }

    public void layout() {
      layoutCollection(myCell);
      newLine(false);
      updatePositions(myCell);
    }


    private void layout(final EditorCell cell) {
      if (isOnNewLine(myCell, cell)) {
        newLine(false);
      }

      if (cell.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT)) {
        withIndent(myCurrentIndent + myIndentSize, myCurrentIndent + 3 * myIndentSize, new Runnable() {
          @Override
          public void run() {
            appendCell(cell, false);
          }
        });
      } else {
        appendCell(cell, false);
      }

      if (haveToSplit()) {
        splitLineAt(findSplitPoint());
      }

      if (isNewLineAfter(myCell, cell)) {
        newLine(false);
      }
    }

    private void withIndent(int indent, int wrapIndent, Runnable r) {
      try {
        myIndentStack.push(myCurrentIndent);
        myWrapStack.push(myCurrentIndentAfterWrap);
        myCurrentIndent = indent;
        myCurrentIndentAfterWrap = wrapIndent;
        r.run();
      } finally {
        myCurrentIndent = myIndentStack.pop();
        myCurrentIndentAfterWrap = myWrapStack.pop();
      }
    }

    private void layoutCollection(final EditorCell_Collection collection) {
      boolean hasIndent = collection != myCell && collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT);
      boolean hasAnchor = collection != myCell && collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_INDENT_ANCHOR);
      boolean hasWrapAnchor = collection.getStyle().get(StyleAttributes.INDENT_LAYOUT_WRAP_ANCHOR);

      int indent = hasIndent && hasAnchor ? currentIndent() + myIndentSize :
          hasIndent ? myCurrentIndent + myIndentSize :
              hasAnchor ? currentIndent() + getFirstChildLeftGap(collection)
                  : myCurrentIndent;
      int wrapIndent = hasWrapAnchor ? currentIndent() + getFirstChildLeftGap(collection) :
          (hasAnchor || hasIndent) ? indent + 2 * myIndentSize
              : myCurrentIndentAfterWrap;

      withIndent(indent, wrapIndent, new Runnable() {
        @Override
        public void run() {
          for (EditorCell child : collection) {
            if (child instanceof EditorCell_Collection && isIndentCollection((EditorCell_Collection) child)) {
              layoutCollection((EditorCell_Collection) child);
            } else {
              layout(child);
            }
          }
        }
      });
    }

    private int currentIndent() {
      int indent = myLineWidth;
      if (myLineContent.isEmpty()) {
        indent += myOverflow ? myCurrentIndentAfterWrap : myCurrentIndent;
      }
      return indent;
    }

    private int getFirstChildLeftGap(EditorCell_Collection collection) {
      EditorCell firstLeaf = CellTraversalUtil.getFirstLeaf(collection);
      if (firstLeaf != null) {
        return PunctuationUtil.getLeftGap(firstLeaf);
      }
      return 0;
    }

    private void updatePositions(EditorCell_Collection collection) {
      for (EditorCell child : collection) {
        if (child instanceof EditorCell_Collection && isIndentCollection((EditorCell_Collection) child)) {
          updatePositions((EditorCell_Collection) child);
        }
      }

      int x0 = Integer.MAX_VALUE;
      int y0 = Integer.MAX_VALUE;
      int x1 = Integer.MIN_VALUE;
      int y1 = Integer.MIN_VALUE;

      for (EditorCell child : collection) {
        x0 = Math.min(x0, child.getX());
        y0 = Math.min(y0, child.getY());
        x1 = Math.max(x1, child.getX() + child.getWidth());
        y1 = Math.max(y1, child.getY() + child.getHeight());
      }

      collection.setX(x0);
      collection.setY(y0);
      collection.setWidth(x1 - x0);
      collection.setHeight(y1 - y0);

      //collection is implicitly laid out
      ((EditorCell_Basic) collection).unrequestLayout();

      if (collection != myCell) {
        int ascent = getAscent(collection);
        int descent = collection.getHeight() - ascent;
        collection.setAscent(ascent);
        collection.setDescent(descent);
      }
    }

    private void appendCell(EditorCell cell, boolean last) {
      if (myLineContent.isEmpty()) {
        myLineIndent = myCurrentIndent;
        indent();
      }

      PunctuationUtil.addGaps(cell, myLineContent.isEmpty(), last);

      cell.moveTo(myX + myLineWidth, cell.getY());
      cell.relayout();

      myLineAscent = Math.max(myLineAscent, cell.getAscent());
      myLineDescent = Math.max(myLineDescent, cell.getDescent());
      myTopInset = Math.max(myTopInset, cell.getTopInset());
      myBottomInset = Math.max(myBottomInset, cell.getBottomInset());

      myLineWidth += cell.getWidth();

      myLineContent.add(cell);
      myLineWrapIndent.add(myCurrentIndentAfterWrap);
    }

    private void indent() {
      myLineWidth += myOverflow ? myCurrentIndentAfterWrap : myCurrentIndent;

    }

    private void newLine(boolean overflow) {
      int baseLine = myCell.getY() + myHeight + myTopInset + myLineAscent;

      for (EditorCell cell : myLineContent) {
        cell.setBaseline(baseLine);
        cell.relayout();
      }

      myWidth = Math.max(myWidth, myLineWidth);
      myHeight += myTopInset + myBottomInset + myLineAscent + myLineDescent;
      myOverflow = overflow;

      resetLine();
    }

    private void resetLine() {
      myLineWidth = 0;
      myLineAscent = 0;
      myLineDescent = 0;
      myTopInset = 0;
      myBottomInset = 0;
      myLineContent.clear();
      myLineWrapIndent.clear();
    }

    private boolean haveToSplit() {
      return myX + myLineWidth > myMaxWidth && myLineContent.size() > 1;
    }

    private EditorCell findSplitPoint() {
      EditorCell lastCell = myLineContent.get(myLineContent.size() - 1);
      EditorCell result = lastCell;

      EditorCell current = result;

      while (true) {
        if (!isIndentCollection(current.getParent())) {
          break;
        }

        EditorCell indentLeaf = getFirstIndentLeaf(current.getParent());
        EditorCell unitStart = expandToUnitStart(indentLeaf);

        if (myLineContent.contains(unitStart) && isOnRightSide(unitStart) &&
            cellRangeFitsOnOneLine(unitStart, lastCell)) {

          result = indentLeaf;
          current = current.getParent();
        } else {
          break;
        }
      }

      return expandToUnitStart(result);
    }

    private EditorCell expandToUnitStart(EditorCell cell) {
      EditorCell result = cell;

      while (true) {
        EditorCell prevLeaf = CellTraversalUtil.getPrevLeaf(result);
        // taking into account prevLeafs located inside collections with non-indent layouts:
        // in this case topmost collection itself will be included into myLineContent as a
        // child element 
        while (prevLeaf != null && !myLineContent.contains(prevLeaf)) {
          prevLeaf = prevLeaf.getParent();
        }

        if (!myCell.isAncestorOf(prevLeaf)) {
          break;
        }
        if (!myLineContent.contains(prevLeaf)) {
          break;
        }

        if (isNoWrap(result) || result.getStyle().get(StyleAttributes.PUNCTUATION_LEFT)) {
          result = prevLeaf;
        } else {
          break;
        }
      }

      return result;
    }

    private Boolean isNoWrap(EditorCell current) {
      while (current != null) {
        if (current.getStyle().get(StyleAttributes.INDENT_LAYOUT_NO_WRAP)) {
          return true;
        }
        if (current.getParent().firstCell() == current) {
          current = current.getParent();
        } else {
          return false;
        }
      }
      return false;
    }

    private boolean cellRangeFitsOnOneLine(EditorCell firstCell, EditorCell lastCell) {
      return lastCell.getX() + lastCell.getWidth() - firstCell.getX() <
          myMaxWidth - myX - myCurrentIndentAfterWrap;
    }

    private boolean isOnRightSide(EditorCell cell) {
      return cell.getX() + cell.getWidth() - myX > myMaxWidth / 2;
    }

    private EditorCell getFirstIndentLeaf(EditorCell_Collection collection) {
      if (!isIndentCollection(collection)) {
        return collection;
      }

      EditorCell firstChild = collection.firstCell();
      if (firstChild instanceof EditorCell_Collection) {
        return getFirstIndentLeaf((EditorCell_Collection) firstChild);
      }

      return firstChild;
    }

    private void splitLineAt(EditorCell splitAt) {
      int index = myLineContent.indexOf(splitAt);
      if (index == -1) {
        throw new IllegalStateException();
      }

      final List<EditorCell> oldLine = new ArrayList<EditorCell>(myLineContent.subList(0, index));
      final List<EditorCell> newLine = new ArrayList<EditorCell>(myLineContent.subList(index, myLineContent.size()));

      withIndent(myLineIndent, myLineWrapIndent.get(index), new Runnable() {
        @Override
        public void run() {
          resetLine();

          for (EditorCell cell : oldLine) {
            appendCell(cell, cell == oldLine.get(oldLine.size() - 1));
          }

          if (!oldLine.isEmpty()) {
            newLine(true);
          }

          for (EditorCell cell : newLine) {
            appendCell(cell, false);
          }
        }
      });
    }
  }

}
