/*
 * 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.cells;


import jetbrains.mps.editor.runtime.style.Measure;
import jetbrains.mps.editor.runtime.style.Padding;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.editor.runtime.style.StyleImpl;
import jetbrains.mps.nodeEditor.EditorSettings;
import jetbrains.mps.openapi.editor.style.Style;
import jetbrains.mps.openapi.editor.style.StyleAttribute;
import jetbrains.mps.openapi.editor.style.StyleChangeEvent;
import jetbrains.mps.openapi.editor.style.StyleListener;
import jetbrains.mps.openapi.editor.style.StyleRegistry;
import org.jetbrains.annotations.NotNull;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.util.Set;

public class TextLine {
  // COLORS: Remove hardcoded color
  private static final Color ERROR_COLOR =
      StyleRegistry.getInstance() != null && StyleRegistry.getInstance().isDarkTheme() ? StyleRegistry.getInstance().getEditorBackground() :
          new Color(255, 220, 220);

  private String myText;
  private int myDescent = 0;

  private Font myFont = EditorSettings.getInstance().getDefaultEditorFont();
  private FontMetrics myFontMetrics;

  private int myCaretPosition = 0;
  private int myCaretX = -1;
  private int mySelectionStartX = -1;
  private int mySelectionEndX = -1;
  private int myTextEndX = -1;
  private int myMinWidth = -1;
  private int myStartTextSelectionPosition = 0;
  private int myEndTextSelectionPosition = 0;

  private int myWidth = 0;
  private int myHeight = 0;
  private int myTextHeight = 0;
  private boolean myCaretEnabled = true;
  private int myMinimalLength = 0;

  private double myLineSpacing = EditorSettings.getInstance().getLineSpacing();
  private Color mySelectedTextColor = EditorSettings.getInstance().getSelectionForegroundColor();
  private Color myTextSelectedTextColor = EditorSettings.getInstance().getSelectionForegroundColor();
  private Color myTextSelectedBackgroundColor = EditorSettings.getInstance().getSelectionBackgroundColor();

  private Color myErrorColor = Color.red;

  private boolean myShowsErrorColor = false;

  private boolean myNull;
  private Style myStyle;

  private int myPaddingLeft;
  private int myPaddingRight;
  private int myPaddingTop;
  private int myPaddingBottom;

  private boolean myControlOvered;
  private boolean myStrikeOut;
  private boolean myUnderlined;

  private Color myTextColor;
  private Color myNullTextColor;
  private Color myTextBackground;
  private Color myNullTextBackground;
  private Color mySelectedTextBackground;
  private Color myNulLSelectedTextBackground;

  private boolean myShowCaret;
  private boolean mySelected;
  private boolean myInitialized;
  private int myFontCorrectionRightGap;
  private int myFontCorrectionTextShift;

  public TextLine(String text) {
    this(text, new StyleImpl(), false);
  }

  public TextLine(String text, @NotNull Style style, boolean isNull) {
    setText(text);
    myNull = isNull;
    myStyle = style;
    showTextColor();
  }

  public String getText() {
    return myText;
  }

  public void setText(String text) {
    if (text == null) {
      text = "";
    }

    if (text.equals(myText)) {
      return;
    }
    doSetText(text);
    doSetCaretPosition(Math.min(myText.length(), getCaretPosition()));

    setStartTextSelectionPosition(getCaretPosition());
    setEndTextSelectionPosition(getCaretPosition());
  }

  private void doSetText(String text) {
    myText = text;
    myCaretX = -1;
    mySelectionStartX = -1;
    mySelectionEndX = -1;
    myTextEndX = -1;
  }

  public String getTextBeforeCaret() {
    return myText.substring(0, getCaretPosition());
  }

  public String getTextAfterCaret() {
    return myText.substring(getCaretPosition(), myText.length());
  }

  public int getWidth() {
    return myWidth;
  }

  public int getHeight() {
    return myHeight;
  }

  public int getStartTextSelectionPosition() {
    return myStartTextSelectionPosition;
  }

  public int getEndTextSelectionPosition() {
    return myEndTextSelectionPosition;
  }

  /**
   * @param length Minimal size of the text edit field in chars.
   */
  public void setMinimalLength(int length) {
    myMinimalLength = length;
    myMinWidth = -1;
  }

  private void updateStyle(Set<StyleAttribute> attributes) {
    if (attributes == null || attributes.contains(StyleAttributes.FONT_SIZE) || attributes.contains(StyleAttributes.FONT_STYLE)) {
      //this is the most expensive calculation
      EditorSettings settings = EditorSettings.getInstance();
      Integer styleFontSize = myStyle.get(StyleAttributes.FONT_SIZE);
      String family = settings.getFontFamily();
      Integer style = myStyle.get(StyleAttributes.FONT_STYLE);
      int fontSize = styleFontSize != null ? styleFontSize : settings.getFontSize();
      myFont = FontRegistry.getInstance().getFont(family, style, fontSize);
      myFontMetrics = null;
      myFontCorrectionRightGap = FontRegistry.getInstance().isFakeItalic(family, style) ? 1 : 0;
      myFontCorrectionTextShift = (style & Font.ITALIC) > 0 ? -1 : 0;
    }

    myPaddingLeft = getHorizontalInternalInset(myStyle.get(StyleAttributes.PADDING_LEFT));
    myPaddingRight = getHorizontalInternalInset(myStyle.get(StyleAttributes.PADDING_RIGHT));
    myPaddingTop = getVerticalInternalInset(myStyle.get(StyleAttributes.PADDING_TOP));
    myPaddingBottom = getVerticalInternalInset(myStyle.get(StyleAttributes.PADDING_BOTTOM));

    myControlOvered = myStyle.get(StyleAttributes.CONTROL_OVERED_REFERENCE);
    myStrikeOut = myStyle.get(StyleAttributes.STRIKE_OUT);
    myUnderlined = myStyle.get(StyleAttributes.UNDERLINED);

    myTextColor = myStyle.get(StyleAttributes.TEXT_COLOR);
    myNullTextColor = myStyle.get(StyleAttributes.NULL_TEXT_COLOR);
    myTextBackground = myStyle.get(StyleAttributes.TEXT_BACKGROUND_COLOR);
    myNullTextBackground = myStyle.get(StyleAttributes.NULL_TEXT_BACKGROUND_COLOR);
    mySelectedTextBackground = myStyle.get(StyleAttributes.SELECTED_TEXT_BACKGROUND_COLOR);
    myNulLSelectedTextBackground = myStyle.get(StyleAttributes.NULL_SELECTED_TEXT_BACKGROUND_COLOR);
  }

  private void init() {
    if (myInitialized) {
      return;
    }
    myInitialized = true;
    updateStyle(null);
    myStyle.addListener(e -> {
      Set<StyleAttribute> changedAttributes = e.getChangedAttributes();
      updateStyle(changedAttributes);
    });

  }

  public void relayout() {
    FontMetrics metrics = getFontMetrics();
    myHeight = (int) (metrics.getHeight() * myLineSpacing + getPaddingTop() + getPaddingBottom());
    myTextHeight = (int) (metrics.getHeight() * myLineSpacing);
    int minWidth = calculateMinWidth();
    int width =
        metrics.charsWidth(myText.toCharArray(), 0, myText.length()) + myFontCorrectionRightGap + getPaddingLeft() + getPaddingRight();
    myWidth = Math.max(minWidth, width);
    myDescent = metrics.getDescent();
  }

  int getEffectiveWidth() {
    int minWidth = calculateMinWidth();
    int effectiveWidth = myWidth - getPaddingLeft() - getPaddingRight();
    return Math.max(minWidth, effectiveWidth);
  }

  private int calculateMinWidth() {
    if (myMinWidth == -1) {
      myMinWidth = Math.max(myMinimalLength * getFontMetrics().charWidth('w'), 2);
    }
    return myMinWidth;
  }

  private int getHorizontalInternalInset(Padding p) {
    double value = p.getValue();
    Measure type = p.getType();

    if (type == null) {
      type = Measure.SPACES;
    }

    if (type.equals(Measure.SPACES)) {
      return (int) (charWidth() * value);
    }
    if (type.equals(Measure.PIXELS)) {
      return (int) value;
    }
    return 0;
  }

  private int getVerticalInternalInset(Padding p) {
    double value = p.getValue();
    Measure type = p.getType();

    if (type == null) {
      type = Measure.SPACES;
    }

    if (type.equals(Measure.SPACES)) {
      return (int) (charHeight() * value);
    }
    if (type.equals(Measure.PIXELS)) {
      return (int) value;
    }
    return 0;
  }

  public int getPaddingLeft() {
    init();
    return myPaddingLeft;
  }

  public int getPaddingRight() {
    init();
    return myPaddingRight;
  }

  public int getPaddingTop() {
    init();
    return myPaddingTop;
  }

  public int getPaddingBottom() {
    init();
    return myPaddingBottom;
  }

  public int charWidth() {
    FontMetrics metrics = getFontMetrics();
    return metrics.charWidth('w');
  }

  public int charHeight() {
    return getFontMetrics().getHeight();
  }

  public boolean isCaretEnabled() {
    return myCaretEnabled;
  }

  public void setCaretEnabled(boolean caretEnabled) {
    myCaretEnabled = caretEnabled;
  }

  public void home() {
    setCaretPosition(0);
  }

  public void end() {
    setCaretPosition(getText().length());
  }

  public void showErrorColor() {
    myShowsErrorColor = true;
  }

  public void showTextColor() {
    myShowsErrorColor = false;
  }

  public Color getBackgroundColor() {
    if (myShowsErrorColor) {
      return ERROR_COLOR;
    }
    return null;
  }

  public Color getTextColor() {
    init();
    if (myControlOvered) {
      return EditorSettings.getInstance().getHyperlinkColor();
    }

    if (!myNull && myTextColor != null) {
      return myTextColor;
    } else {
      return myNullTextColor;
    }
  }

  public Color getEffectiveTextColor() {
    if (myShowsErrorColor) {
      return myErrorColor;
    } else {
      return getTextColor();
    }
  }

  public Color getEffectiveSelectedTextColor() {
    if (myShowsErrorColor) {
      return ERROR_COLOR;
    } else {
      return mySelectedTextColor != null ? mySelectedTextColor : getTextColor();
    }
  }

  public Color getTextBackgroundColor() {
    init();
    if (myShowsErrorColor) {
      return ERROR_COLOR;
    } else {
      if (!myNull) {
        return myTextBackground;
      } else {
        return myNullTextBackground;
      }
    }
  }

  public void setSelectedTextColor(Color selectedTextColor) {
    mySelectedTextColor = selectedTextColor;
  }


  public Color getSelectedTextBackgroundColor() {
    init();
    if (!myNull) {
      return mySelectedTextBackground;
    } else {
      return myNulLSelectedTextBackground;
    }
  }

  public Font getFont() {
    init();
    return myFont;
  }

  public boolean isSelected() {
    return mySelected;
  }

  public void setSelected(boolean isSelected) {
    mySelected = isSelected;
  }

  public boolean isShowCaret() {
    return myShowCaret;
  }

  public void setShowCaret(boolean showCaret) {
    myShowCaret = showCaret;
  }

  public void paint(Graphics g, int shiftX, int shiftY) {
    paint(g, shiftX, shiftY, null);
  }

  public void paint(Graphics g, int shiftX, int shiftY, Color forcedTextColor) {
    Color backgroundColor;
    Color textColor;
    Color textBackgroundColor;

    backgroundColor = getBackgroundColor();
    if (forcedTextColor != null) {
      textColor = forcedTextColor;
      textBackgroundColor = null;
    } else {
      if (mySelected) {
        textColor = getEffectiveSelectedTextColor();
        textBackgroundColor = getSelectedTextBackgroundColor();
      } else {
        textColor = getEffectiveTextColor();
        textBackgroundColor = getTextBackgroundColor();
      }
    }

    if (backgroundColor != null && !g.getColor().equals(backgroundColor) && !mySelected) {
      g.setColor(backgroundColor);
      g.fillRect(shiftX + getPaddingLeft(),
          shiftY + getPaddingTop(),
          myWidth,
          myTextHeight);
    }

    if (textBackgroundColor != null) {
      g.setColor(textBackgroundColor);
      g.fillRect(shiftX + getPaddingLeft(),
          shiftY + getPaddingTop(),
          myWidth,
          myTextHeight);
    }

    g.setFont(getFont());
    if (!g.getColor().equals(textColor)) {
      g.setColor(textColor);
    }

    int selectionStartX = shiftX + getPaddingLeft() + getSelectionStartX();
    int selectionEndX = shiftX + getPaddingLeft() + getSelectionEndX();
    int endLineX = shiftX + getPaddingLeft() + getTextEndX();
    int baselineY = shiftY + myHeight - myDescent - getPaddingBottom() - getPaddingTop();
    int centerLineY = shiftY + (myHeight - getPaddingBottom() + getPaddingTop()) / 2;

    if (getStartTextSelectionPosition() > 0) {
      g.drawString(myText.substring(0, getStartTextSelectionPosition()), shiftX + getPaddingLeft() + myFontCorrectionTextShift, baselineY);
      if (isUnderlined()) {
        g.drawLine(shiftX + getPaddingLeft(), baselineY + 1, selectionStartX, baselineY + 1);
      }
      if (isStrikeOut()) {
        drawStrikeOutLine(g, shiftX + getPaddingLeft(), selectionStartX, centerLineY);
      }
    }

    if (getEndTextSelectionPosition() <= myText.length()) {
      g.drawString(myText.substring(getEndTextSelectionPosition()), selectionEndX + myFontCorrectionTextShift, baselineY);
      if (isUnderlined()) {
        g.drawLine(selectionEndX, baselineY + 1, endLineX, baselineY + 1);
      }
      if (isStrikeOut()) {
        drawStrikeOutLine(g, selectionEndX, endLineX, centerLineY);
      }
    }

    if (getStartTextSelectionPosition() < getEndTextSelectionPosition()) {
      //drawing textual selection
      String selectedText = getTextuallySelectedText();
      g.setColor(myTextSelectedBackgroundColor);
      // Filling smaller rectangle to not cover frames created by other messages
      if (selectionEndX - selectionStartX - 2 + myFontCorrectionRightGap > 0) {
        g.fillRect(selectionStartX + 1, shiftY + getPaddingTop() + 1,
            selectionEndX - selectionStartX - 2 + myFontCorrectionRightGap, myTextHeight - 2);
      }

      g.setColor(myTextSelectedTextColor != null ? myTextSelectedTextColor : getTextColor());
      g.drawString(selectedText, selectionStartX + myFontCorrectionTextShift, baselineY);
      if (isUnderlined()) {
        g.drawLine(selectionStartX, baselineY + 1, selectionEndX, baselineY + 1);
      }
      if (isStrikeOut()) {
        drawStrikeOutLine(g, selectionStartX, selectionEndX, centerLineY);
      }

      g.setColor(textColor);
    }

    if (myShowCaret) {
      drawCaret(g, shiftX, shiftY);
    }
  }

  private void drawStrikeOutLine(Graphics g, int beginX, int endX, int constY) {
    g.drawLine(beginX, constY + 1, endX, constY + 1);
  }

  private void drawCaret(Graphics g, int shiftX, int shiftY) {
    if (!myCaretEnabled) {
      return;
    }
    int x = getCaretX(shiftX);
    if (getCaretPosition() != 0) {
      x--;
    }
    g.setColor(EditorSettings.getInstance().getCaretColor());
    g.fillRect(x, shiftY, 2, myTextHeight);
  }

  public void repaintCaret(Component component, int shiftX, int shiftY) {
    int x = getCaretX(shiftX);
    component.repaint(x - 1, shiftY - 1, x + 2, myTextHeight + 2);
  }

  public int getCaretX(int shiftX) {
    if (myCaretX == -1) {
      myCaretX = getTextWidth(getCaretPosition());
    }
    return shiftX + getPaddingLeft() + myCaretX;
  }

  private int getSelectionStartX() {
    if (mySelectionStartX == -1) {
      mySelectionStartX = getTextWidth(getStartTextSelectionPosition());
    }
    return mySelectionStartX;
  }

  private int getSelectionEndX() {
    if (mySelectionEndX == -1) {
      mySelectionEndX = getTextWidth(getEndTextSelectionPosition());
    }
    return mySelectionEndX;
  }

  private int getTextEndX() {
    if (myTextEndX == -1) {
      myTextEndX = getTextWidth(getText().length());
    }
    return myTextEndX;
  }

  private int getTextWidth(int caretPosition) {
    FontMetrics metrics = getFontMetrics();
    return metrics.charsWidth(myText.toCharArray(), 0, caretPosition);
  }

  public FontMetrics getFontMetrics() {
    if (myFontMetrics == null) {
      myFontMetrics = FontRegistry.getInstance().getFontMetrics(getFont());
    }
    return myFontMetrics;
  }

  public String getTextuallySelectedText() {
    if (getStartTextSelectionPosition() > getEndTextSelectionPosition()) {
      return "";
    }
    return myText.substring(getStartTextSelectionPosition(), getEndTextSelectionPosition());
  }


  public void resetSelection() {
    setStartTextSelectionPosition(getCaretPosition());
    setEndTextSelectionPosition(getCaretPosition());
  }

  public boolean hasNonTrivialSelection() {
    return (getStartTextSelectionPosition() != getCaretPosition() || getEndTextSelectionPosition() != getCaretPosition());
  }

  public void setStartTextSelectionPosition(int i) {
    assert i >= 0;
    this.myStartTextSelectionPosition = Math.min(i, myText.length());
    mySelectionStartX = -1;
  }

  public void setEndTextSelectionPosition(int i) {
    assert i >= 0;
    this.myEndTextSelectionPosition = Math.min(i, myText.length());
    mySelectionEndX = -1;
  }

  public void selectAll() {
    setStartTextSelectionPosition(0);
    setEndTextSelectionPosition(getText().length());
  }

  public void deselectAll() {
    setStartTextSelectionPosition(getCaretPosition());
    setEndTextSelectionPosition(getCaretPosition());
  }

  public boolean isEverythingSelected() {
    return getStartTextSelectionPosition() == 0 && getEndTextSelectionPosition() == getText().length();
  }

  public void setCaretByXCoord(int x) {
    setCaretPosition(getCaretPositionByXCoord(x));
  }

  public int getCaretPositionByXCoord(int _x) {
    int x = _x - getPaddingLeft();
    FontMetrics metrics = getFontMetrics();
    char[] chars = getText().toCharArray();
    int caretPosition = myText.length();
    int len = 0;
    for (int i = 0; i < myText.length(); i++) {
      int newLen = metrics.charsWidth(chars, 0, i + 1);
      if (x <= (len + newLen + 1) / 2) {
        caretPosition = i;
        break;
      }
      len = newLen;
    }
    return caretPosition;
  }

  public int getCaretPosition() {
    return myCaretPosition;
  }

  public void setCaretPosition(int i) {
    setCaretPosition(i, false);
  }

  private void doSetCaretPosition(int position) {
    myCaretPosition = position;
    myCaretX = -1;
  }

  public void setCaretPosition(int position, boolean duringSelection) {
    assert position >= 0;
    if (!duringSelection) {
      doSetCaretPosition(Math.min(myText.length(), position));
      setStartTextSelectionPosition(getCaretPosition());
      setEndTextSelectionPosition(getCaretPosition());
      return;
    }

    int old = getCaretPosition();
    doSetCaretPosition(Math.min(myText.length(), position));

    if (getEndTextSelectionPosition() == old) {
      setEndTextSelectionPosition(getCaretPosition());
    } else {
      setStartTextSelectionPosition(getCaretPosition());
    }

    if (getEndTextSelectionPosition() < getStartTextSelectionPosition()) {
      int temp = getEndTextSelectionPosition();
      setEndTextSelectionPosition(getStartTextSelectionPosition());
      setStartTextSelectionPosition(temp);
    }
  }

  public boolean isUnderlined() {
    init();
    if (myControlOvered) {
      return true;
    }

    return myUnderlined;
  }

  public boolean isStrikeOut() {
    init();
    return myStrikeOut;
  }

  public int getAscent() {
    return myTextHeight - myDescent;
  }

  public int getDescent() {
    return myDescent;
  }
}
