/*
 * Copyright 2000-2017 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 com.intellij.openapi.editor.impl.view;

import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.FoldingModelImpl;
import com.intellij.openapi.editor.impl.SoftWrapModelImpl;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType;
import com.intellij.util.DocumentUtil;
import org.jetbrains.annotations.NotNull;

import java.awt.geom.Point2D;
import java.util.List;

/**
 * Performs transformations between various location representations in editor 
 * (offset, logical position, visual position, pixel coordinates).
 * 
 * @see LogicalPosition
 * @see VisualPosition
 */
class EditorCoordinateMapper {
  private static final Logger LOG = Logger.getInstance(EditorCoordinateMapper.class);

  private final EditorView myView;
  private final Document myDocument;
  private final FoldingModelImpl myFoldingModel;
  
  EditorCoordinateMapper(EditorView view) {
    myView = view;
    myDocument = myView.getEditor().getDocument();
    myFoldingModel = myView.getEditor().getFoldingModel();
  }

  int visualLineToY(int line) {
    return myView.getInsets().top + Math.max(0, line) * myView.getLineHeight();
  }

  int yToVisualLine(int y) {
    return Math.max(0, y - myView.getInsets().top) / myView.getLineHeight();
  }

  @NotNull
  LogicalPosition offsetToLogicalPosition(int offset) {
    return myView.getLogicalPositionCache().offsetToLogicalPosition(offset);
  }

  int logicalPositionToOffset(@NotNull LogicalPosition pos) {
    return myView.getLogicalPositionCache().logicalPositionToOffset(pos);
  }

  @NotNull
  VisualPosition logicalToVisualPosition(@NotNull LogicalPosition pos, boolean beforeSoftWrap) {
    int line = pos.line;
    int column = pos.column;
    int logicalLineCount = myDocument.getLineCount();
    if (line >= logicalLineCount) {
      return new VisualPosition(line - logicalLineCount + myView.getEditor().getVisibleLineCount(), column, pos.leansForward);
    }
    int offset = logicalPositionToOffset(pos);
    int visualLine = offsetToVisualLine(offset, beforeSoftWrap);
    int maxVisualColumn = 0;
    int maxLogicalColumn = 0;
    for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap)) {
      if (!pos.leansForward && offset == fragment.getVisualLineStartOffset()) {
        return new VisualPosition(visualLine, fragment.getStartVisualColumn());
      }
      if (fragment.isCollapsedFoldRegion()) {
        int startLogicalLine = fragment.getStartLogicalLine();
        int endLogicalLine = fragment.getEndLogicalLine();
        int startLogicalColumn = fragment.getStartLogicalColumn();
        int endLogicalColumn = fragment.getEndLogicalColumn();
        if ((line > startLogicalLine || line == startLogicalLine && (column > startLogicalColumn ||
                                                                     column == startLogicalColumn && pos.leansForward)) &&
            (line < endLogicalLine || line == endLogicalLine && column < endLogicalColumn)) {
          return new VisualPosition(visualLine, fragment.getStartVisualColumn(), true);
        }
        if (line == endLogicalLine && column == endLogicalColumn && !pos.leansForward) {
          return new VisualPosition(visualLine, fragment.getEndVisualColumn());
        }
        maxLogicalColumn = startLogicalLine == endLogicalLine ? Math.max(maxLogicalColumn, endLogicalColumn) : endLogicalColumn;
      }
      else if (fragment.getCurrentInlays() == null) {
        int minColumn = fragment.getMinLogicalColumn();
        int maxColumn = fragment.getMaxLogicalColumn();
        if (line == fragment.getStartLogicalLine() &&
            (column > minColumn && column < maxColumn ||
             column == minColumn && pos.leansForward ||
             column == maxColumn && !pos.leansForward)) {
          return new VisualPosition(visualLine, fragment.logicalToVisualColumn(column), fragment.isRtl() ^ pos.leansForward);
        }
        maxLogicalColumn = Math.max(maxLogicalColumn, maxColumn);
      }
      maxVisualColumn = fragment.getEndVisualColumn();
    }
    int resultColumn = column - maxLogicalColumn + maxVisualColumn;
    if (resultColumn < 0) {
      if (maxVisualColumn > maxLogicalColumn) {
        resultColumn = Integer.MAX_VALUE; // guarding against overflow
      }
      else {
        LOG.error("Error converting " + pos + " to visual position",
                  new Attachment("details.txt", String.format("offset: %d, visual line: %d, max logical column: %d, max visual column: %d",
                                                              offset, visualLine, maxLogicalColumn, maxVisualColumn)),
                  new Attachment("dump.txt", myView.getEditor().dumpState()));
        resultColumn = 0;
      }
    }
    return new VisualPosition(visualLine, resultColumn, pos.leansForward);
  }

  @NotNull
  LogicalPosition visualToLogicalPosition(@NotNull VisualPosition pos) {
    int line = pos.line;
    int column = pos.column;
    int visualLineCount = myView.getEditor().getVisibleLineCount();
    if (line >= visualLineCount) {
      return new LogicalPosition(line - visualLineCount + myDocument.getLineCount(), column, pos.leansRight);
    }
    int offset = visualLineToOffset(line);
    int logicalLine = myDocument.getLineNumber(offset);
    int maxVisualColumn = 0;
    int maxLogicalColumn = 0;
    int maxOffset = offset;
    for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false)) {
      int minColumn = fragment.getStartVisualColumn();
      int maxColumn = fragment.getEndVisualColumn();
      if (column < minColumn || column == minColumn && !pos.leansRight) {
        return offsetToLogicalPosition(offset);
      }
      if (column > minColumn && column < maxColumn ||
          column == minColumn ||
          column == maxColumn && !pos.leansRight) {
        return new LogicalPosition(column == maxColumn ? fragment.getEndLogicalLine() : fragment.getStartLogicalLine(), 
                                   fragment.visualToLogicalColumn(column),
                                   fragment.isCollapsedFoldRegion() ? column < maxColumn :
                                   fragment.getCurrentInlays() != null ? column == maxColumn :
                                   fragment.isRtl() ^ pos.leansRight);
      }
      maxLogicalColumn = logicalLine == fragment.getEndLogicalLine() ? Math.max(maxLogicalColumn, fragment.getMaxLogicalColumn()) : 
                         fragment.getMaxLogicalColumn();
      maxVisualColumn = maxColumn;
      logicalLine = fragment.getEndLogicalLine();
      maxOffset = Math.max(maxOffset, fragment.getMaxOffset());
    }
    if (myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) == null) {
      int resultColumn = column - maxVisualColumn + maxLogicalColumn;
      if (resultColumn < 0 && maxLogicalColumn > maxVisualColumn) {
        resultColumn = Integer.MAX_VALUE; // guarding against overflow
      }
      return new LogicalPosition(logicalLine, resultColumn, true);
    }
    else {
      return offsetToLogicalPosition(maxOffset).leanForward(true);
    }
  }

  @NotNull
  VisualPosition offsetToVisualPosition(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) {
    return logicalToVisualPosition(offsetToLogicalPosition(offset).leanForward(leanTowardsLargerOffsets), beforeSoftWrap);
  }

  int visualPositionToOffset(VisualPosition visualPosition) {
    return logicalPositionToOffset(visualToLogicalPosition(visualPosition));
  }

  int offsetToVisualLine(int offset, boolean beforeSoftWrap) {
    int textLength = myDocument.getTextLength();
    if (offset < 0 || textLength == 0) {
      return 0;
    }
    offset = Math.min(offset, textLength);
    offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset);

    FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset);
    if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) {
      assert outermostCollapsed.isValid();
      offset = outermostCollapsed.getStartOffset();
      beforeSoftWrap = false;
    }

    int wrapIndex = myView.getEditor().getSoftWrapModel().getSoftWrapIndex(offset);
    int softWrapsBeforeOrAtOffset = wrapIndex < 0 ? (- wrapIndex - 1) : wrapIndex + (beforeSoftWrap ? 0 : 1);

    return myDocument.getLineNumber(offset) - myFoldingModel.getFoldedLinesCountBefore(offset) + softWrapsBeforeOrAtOffset;
  }

  int visualLineToOffset(int visualLine) {
    int start = 0;
    int end = myDocument.getTextLength();
    if (visualLine <= 0) return start;
    if (visualLine >= myView.getEditor().getVisibleLineCount()) return end;
    int current = 0;
    while (start <= end) {
      current = (start + end) / 2;
      int line = offsetToVisualLine(current, false);
      if (line < visualLine) {
        start = current + 1;
      }
      else if (line > visualLine) {
        end = current - 1;
      }
      else {
        break;
      }
    }
    return visualLineStartOffset(current, true);
  }

  private int visualLineStartOffset(int offset, boolean leanForward) {
    EditorImpl editor = myView.getEditor();
    offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset);
    int result = EditorUtil.getNotFoldedLineStartOffset(editor, offset);

    SoftWrapModelImpl softWrapModel = editor.getSoftWrapModel();
    List<? extends SoftWrap> softWraps = softWrapModel.getRegisteredSoftWraps();
    int currentOrPrevWrapIndex = softWrapModel.getSoftWrapIndex(offset);
    SoftWrap currentOrPrevWrap;
    if (currentOrPrevWrapIndex < 0) {
      currentOrPrevWrapIndex = - currentOrPrevWrapIndex - 2;
      currentOrPrevWrap = currentOrPrevWrapIndex < 0 || currentOrPrevWrapIndex >= softWraps.size() ? null :
                          softWraps.get(currentOrPrevWrapIndex);
    }
    else {
      currentOrPrevWrap = leanForward ? softWraps.get(currentOrPrevWrapIndex) : null;
    }
    if (currentOrPrevWrap != null && currentOrPrevWrap.getStart() > result) {
      result = currentOrPrevWrap.getStart();
    }
    return result;
  }

  private float getStartX(int line) {
    return myView.getInsets().left + (line == 0 ? myView.getPrefixTextWidthInPixels() : 0);
  }

  @NotNull
  VisualPosition xyToVisualPosition(@NotNull Point2D p) {
    int visualLine = yToVisualLine((int)p.getY());
    int lastColumn = 0;
    float x = getStartX(visualLine);
    float px = (float)p.getX();
    if (visualLine < myView.getEditor().getVisibleLineCount()) {
      int visualLineStartOffset = visualLineToOffset(visualLine);
      int maxOffset = 0;
      for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false)) {
        if (px <= fragment.getStartX()) {
          if (fragment.getStartVisualColumn() == 0) {
            return new VisualPosition(visualLine, 0);
          }
          int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP);
          float indent = fragment.getStartX() - markerWidth;
          if (px <= indent) {
            break;
          }
          boolean after = px >= indent + markerWidth / 2;
          return new VisualPosition(visualLine, fragment.getStartVisualColumn() - (after ? 0 : 1), !after);
        }
        float nextX = fragment.getEndX();
        if (px <= nextX) {
          int[] column = fragment.xToVisualColumn(px);
          return new VisualPosition(visualLine, column[0], column[1] > 0);
        }
        x = nextX;
        lastColumn = fragment.getEndVisualColumn();
        maxOffset = Math.max(maxOffset, fragment.getMaxOffset());
      }
      if (myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) != null) {
        int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
        if (px <= x + markerWidth) {
          boolean after = px >= x + markerWidth / 2;
          return new VisualPosition(visualLine, lastColumn + (after ? 1 : 0), !after);
        }
        px -= markerWidth;
        lastColumn++;
      }
    }
    int plainSpaceWidth = myView.getPlainSpaceWidth();
    int remainingShift = (int)(px - x);
    int additionalColumns = remainingShift <= 0 ? 0 : (remainingShift + plainSpaceWidth / 2) / plainSpaceWidth;
    return new VisualPosition(visualLine, lastColumn + additionalColumns, 
                              remainingShift > 0 && additionalColumns == (remainingShift - 1) / plainSpaceWidth);
  }

  @NotNull
  Point2D visualPositionToXY(@NotNull VisualPosition pos) {
    int visualLine = pos.line;
    int column = pos.column;
    int y = visualLineToY(visualLine);
    float x = getStartX(visualLine);
    int lastColumn = 0;
    if (visualLine < myView.getEditor().getVisibleLineCount()) {
      int visualLineStartOffset = visualLineToOffset(visualLine);
      int maxOffset = 0;
      for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false)) {
        int startVisualColumn = fragment.getStartVisualColumn();
        if (column < startVisualColumn || column == startVisualColumn && !pos.leansRight) {
          break;
        }
        int endColumn = fragment.getEndVisualColumn();
        if (column < endColumn || column == endColumn && !pos.leansRight) {
          return new Point2D.Float(fragment.visualColumnToX(column), y);
        }
        x = fragment.getEndX();
        lastColumn = endColumn;
        maxOffset = Math.max(maxOffset, fragment.getMaxOffset());
      }
      if (column > lastColumn && myView.getEditor().getSoftWrapModel().getSoftWrap(maxOffset) != null) {
        column--;
        x += myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
      }
    }
    int additionalShift = column <= lastColumn ? 0 : (column - lastColumn) * myView.getPlainSpaceWidth();
    return new Point2D.Float(x + additionalShift, y);
  }

  @NotNull
  Point2D offsetToXY(int offset, boolean leanTowardsLargerOffsets, boolean beforeSoftWrap) {
    offset = Math.max(0, Math.min(myDocument.getTextLength(), offset));
    offset = DocumentUtil.alignToCodePointBoundary(myDocument, offset);
    int logicalLine = myDocument.getLineNumber(offset);
    int visualLine = offsetToVisualLine(offset, beforeSoftWrap);
    int visualLineStartOffset = visualLineToOffset(visualLine);
    int y = visualLineToY(visualLine);
    float x = getStartX(logicalLine);
    if (myDocument.getTextLength() > 0) {
      boolean firstFragment = true;
      for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap)) {
        if (firstFragment && offset == visualLineStartOffset && !leanTowardsLargerOffsets) {
          x = fragment.getStartX();
          break;
        }
        firstFragment = false;
        int minOffset = fragment.getMinOffset();
        int maxOffset = fragment.getMaxOffset();
        if (fragment.getCurrentInlays() == null &&
            (offset > minOffset && offset < maxOffset ||
            offset == minOffset && leanTowardsLargerOffsets ||
            offset == maxOffset && !leanTowardsLargerOffsets)) {
          x = fragment.offsetToX(offset);
          break;
        }
        else {
          x = fragment.getEndX();
        }
      }
    }
    return new Point2D.Float(x, y);
  }
}
