/*
 * Copyright (c) 2010-2020, sikuli.org, sikulix.com - MIT license
 */
package org.sikuli.ide;

import org.sikuli.basics.PreferencesUser;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.Map;
import javax.swing.*;
import javax.swing.text.*;

import org.sikuli.basics.Debug;
import org.sikuli.support.ide.IIndentationLogic;

public class SikuliEditorKit extends StyledEditorKit {

  private static final String me = "EditorKit: ";
  private ViewFactory _viewFactory;
  private static EditorPane pane;

  public static final String deIndentAction = "SKL.DeindentAction";
  public static final String completionAction = "SKL.CompletionAction";
  private static final TextAction[] defaultActions = {
          new InsertTabAction(),
          new DeindentAction(),
          new InsertBreakAction(),
          new CompletionAction(),
          new NextVisualPositionAction(forwardAction, false, SwingConstants.EAST),
          new NextVisualPositionAction(backwardAction, false, SwingConstants.WEST),
          new NextVisualPositionAction(selectionForwardAction, true, SwingConstants.EAST),
          new NextVisualPositionAction(selectionBackwardAction, true, SwingConstants.WEST),
          new NextVisualPositionAction(upAction, false, SwingConstants.NORTH),
          new NextVisualPositionAction(downAction, false, SwingConstants.SOUTH),
          new NextVisualPositionAction(selectionUpAction, true, SwingConstants.NORTH),
          new NextVisualPositionAction(selectionDownAction, true, SwingConstants.SOUTH),
          new NextVisualLineAction(endLineAction, false, SwingConstants.EAST),
          new NextVisualLineAction(beginLineAction, false, SwingConstants.WEST),
          new NextVisualLineAction(selectionEndLineAction, true, SwingConstants.EAST),
          new NextVisualLineAction(selectionBeginLineAction, true, SwingConstants.WEST),
  };

  private static SikuliIDEPopUpMenu popCompletion = null;

  public SikuliEditorKit() {
    pane = SikulixIDE.get().getCurrentCodePane();
    _viewFactory = new EditorViewFactory();
    ((EditorViewFactory) _viewFactory).setContentType(pane.getType());
    popCompletion = pane.getPopMenuCompletion();
    if (null == popCompletion || !popCompletion.isValidMenu()) {
      popCompletion = null;
    }

  }

  public static class InsertTabAction extends TextAction {

    private IIndentationLogic indentationLogic;

    public InsertTabAction() {
      super(insertTabAction);
    }

    public InsertTabAction(String name) {
      super(name);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      Debug.log(5, "InsertTabAction " + e);
      JTextComponent text = (JTextComponent) e.getSource();
      actionPerformed(text);
    }

    public void actionPerformed(JTextComponent text) {
      boolean indentError = false;
      Document doc = text.getDocument();
      Element map = doc.getDefaultRootElement();
      String tabWhitespace = PreferencesUser.get().getTabWhitespace();
      Caret c = text.getCaret();
      int dot = c.getDot();
      int mark = c.getMark();
      int dotLine = map.getElementIndex(dot);
      int markLine = map.getElementIndex(mark);

      if (dotLine != markLine) {
        int first = Math.min(dotLine, markLine);
        int last = Math.max(dotLine, markLine);
        Element elem;
        int start;
        try {
          for (int i = first; i < last; i++) {
            elem = map.getElement(i);
            start = elem.getStartOffset();
            doc.insertString(start, tabWhitespace, null);
          }
          elem = map.getElement(last);
          start = elem.getStartOffset();
          if (Math.max(c.getDot(), c.getMark()) != start) {
            doc.insertString(start, tabWhitespace, null);
          }
        } catch (BadLocationException ble) {
          Debug.error(me + "Problem while indenting line\n%s", ble.getMessage());
          UIManager.getLookAndFeel().provideErrorFeedback(text);
        }
      } else {
        text.replaceSelection(tabWhitespace);
      }
    }
  }

  public static class DeindentAction extends TextAction {
//TODO dedent not working consistently on last line (no last empty line)

    private IIndentationLogic indentationLogic;
    private Segment segLine;

    public DeindentAction() {
      this(deIndentAction);
    }

    public DeindentAction(String name) {
      super(name);
      segLine = new Segment();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      Debug.log(5, "DedentAction " + e);
      JTextComponent text = (JTextComponent) e.getSource();
      actionPerformed(text);
    }

    public void actionPerformed(JTextComponent text) {
      indentationLogic = ((EditorPane) text).getIndentationLogic();
      if (indentationLogic == null) {
        return;
      }
      StyledDocument doc = (StyledDocument) text.getDocument();
      Element map = doc.getDefaultRootElement();
      Caret c = text.getCaret();
      int dot = c.getDot();
      int mark = c.getMark();
      int line1 = map.getElementIndex(dot);

      if (dot != mark) {
        int line2 = map.getElementIndex(mark);
        int begin = Math.min(line1, line2);
        int end = Math.max(line1, line2);
        Element elem;
        try {
          for (line1 = begin; line1 < end; line1++) {
            elem = map.getElement(line1);
            handleDecreaseIndent(line1, elem, doc);
          }
          elem = map.getElement(end);
          int start = elem.getStartOffset();
          if (Math.max(c.getDot(), c.getMark()) != start) {
            handleDecreaseIndent(end, elem, doc);
          }
        } catch (BadLocationException ble) {
          Debug.error(me + "Problem while de-indenting line\n%s", ble.getMessage());
          UIManager.getLookAndFeel().provideErrorFeedback(text);
        }
      } else {
        Element elem = map.getElement(line1);
        try {
          handleDecreaseIndent(line1, elem, doc);
        } catch (BadLocationException ble) {
          Debug.error(me + "Problem while de-indenting line\n%s", ble.getMessage());
          UIManager.getLookAndFeel().provideErrorFeedback(text);
        }
      }

    }

    private void handleDecreaseIndent(int line, Element elem, StyledDocument doc)
            throws BadLocationException {
      int start = elem.getStartOffset();
      int end = elem.getEndOffset() - 1;
      doc.getText(start, end - start, segLine);
      int i = segLine.offset;
      end = i + segLine.count;
      if (end > i) {
        String leadingWS = indentationLogic.getLeadingWhitespace(doc, start, end - start);
        int toRemove = indentationLogic.checkDedent(leadingWS, line + 1);
        doc.remove(start, toRemove);
      }
    }
  }

  public static class InsertBreakAction extends TextAction {

    private IIndentationLogic indentationLogic;

    public InsertBreakAction() {
      super(insertBreakAction);
    }

    public InsertBreakAction(String name) {
      super(name);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      Debug.log(5, "InsertBreakAction " + e);
      JTextComponent text = (JTextComponent) e.getSource();
      insertBreak(text);
    }

    public void insertBreak(JTextComponent text) {
      boolean noSelection = text.getSelectionStart() == text.getSelectionEnd();
      if (noSelection) {
        insertNewlineWithAutoIndent(text);
      } else {
        text.replaceSelection("\n");
//TODO insertNewlineWithAutoIndent
      }
    }

    private void insertNewlineWithAutoIndent(JTextComponent text) {
      indentationLogic = ((EditorPane) text).getIndentationLogic();
      if (indentationLogic == null) {
        text.replaceSelection("\n");
        return;
      }
      try {
        int caretPos = text.getCaretPosition();
        StyledDocument doc = (StyledDocument) text.getDocument();
        Element map = doc.getDefaultRootElement();
        int lineNum = map.getElementIndex(caretPos);
        Element line = map.getElement(lineNum);
        int start = line.getStartOffset();
        int end = line.getEndOffset() - 1;
        int len = end - start;
        String s = doc.getText(start, len);
        StringBuilder sb = new StringBuilder("\n");

        String leadingWS = indentationLogic.getLeadingWhitespace(doc, start, caretPos - start);
        sb.append(leadingWS);
//TODO better control over automatic indentation
        indentationLogic.checkIndent(leadingWS, lineNum + 1);

        // If there is only whitespace between the caret and
        // the EOL, pressing Enter auto-indents the new line to
        // the same place as the previous line.
        int nonWhitespacePos = indentationLogic.atEndOfLine(doc, caretPos, start, s, len);
        if (nonWhitespacePos == -1) {
          if (leadingWS.length() == len) {
            // If the line was nothing but whitespace, select it
            // so its contents get removed.
            text.setSelectionStart(start);
          } else {
            // Select the whitespace between the caret and the EOL
            // to remove it
            text.setSelectionStart(caretPos);
          }
          text.setSelectionEnd(end);
          text.replaceSelection(sb.toString());
          // auto-indentation for python statements like if, while, for, try,
          // except, def, class and auto-deindentation for break, continue,
          // pass, return
          analyseDocument(doc, lineNum, indentationLogic);
          // auto-completion: add colon if it is obvious
          if (indentationLogic.shouldAddColon()) {
            doc.insertString(caretPos, ":", null);
            indentationLogic.setLastLineEndsWithColon();
          }
          int lastLineChange = indentationLogic.shouldChangeLastLineIndentation();
          int nextLineChange = indentationLogic.shouldChangeNextLineIndentation();
          if (lastLineChange != 0) {
            Debug.log(5, "change line %d indentation by %d columns", lineNum + 1,
                    lastLineChange);
            changeIndentation((DefaultStyledDocument) doc, lineNum, lastLineChange);
            // nextLineChange was determined based on indentation of last line before
            // the change
            nextLineChange += lastLineChange;
          }
          if (nextLineChange != 0) {
            Debug.log(5, "change line %d indentation by %d columns", lineNum + 2,
                    nextLineChange);
            changeIndentation((DefaultStyledDocument) doc, lineNum + 1, nextLineChange);
          }
        } // If there is non-whitespace between the caret and the
        // EOL, pressing Enter takes that text to the next line
        // and auto-indents it to the same place as the last
        // line. Additional auto-indentation or dedentation for
        // specific python statements is only done for the next line.
        else {
          text.setCaretPosition(nonWhitespacePos);
          doc.insertString(nonWhitespacePos, sb.toString(), null);
          analyseDocument(doc, lineNum, indentationLogic);
          int nextLineChange = indentationLogic.shouldChangeNextLineIndentation();
          if (nextLineChange != 0) {
            Debug.log(5, "change line %d indentation by %d columns", lineNum + 2,
                    nextLineChange);
            changeIndentation((DefaultStyledDocument) doc, lineNum + 1, nextLineChange);
          }
        }

      } catch (BadLocationException ble) {
        text.replaceSelection("\n");
        Debug.error(me + "Problem while inserting new line with auto-indent\n%s", ble.getMessage());
      }

    }

    private void analyseDocument(Document document, int lineNum,
                                 IIndentationLogic indentationLogic) throws BadLocationException {
      Element map = document.getDefaultRootElement();
      int endPos = map.getElement(lineNum).getEndOffset();
      indentationLogic.reset();
      indentationLogic.addText(document.getText(0, endPos));
    }

    /**
     * Change the indentation of a line. Any existing leading whitespace is
     * replaced by the appropriate number of tab characters (padded with blank
     * characters if necessary) if tab expansion in the user preferences is
     * true, or the appropriate number of blank characters if tab expansion is
     * false.
     *
     * @param linenum the line number (0-based)
     * @param columns the number of columns by which to increase the indentation
     *                (if columns is greater than 0) or decrease the indentation (if columns is
     *                less than 0)
     * @throws BadLocationException if the specified line does not exist
     */
    // TODO: make this a method of SikuliDocument, no need to pass document as argument
    private void changeIndentation(DefaultStyledDocument doc, int linenum,
                                   int columns) throws BadLocationException {
      PreferencesUser pref = PreferencesUser.get();
      boolean expandTab = pref.getExpandTab();
      int tabWidth = pref.getTabWidth();

      if (linenum < 0) {
        throw new BadLocationException("Negative line", -1);
      }
      Element map = doc.getDefaultRootElement();
      if (linenum >= map.getElementCount()) {
        throw new BadLocationException("No such line", doc.getLength() + 1);
      }
      if (columns == 0) {
        return;
      }

      Element lineElem = map.getElement(linenum);
      int lineStart = lineElem.getStartOffset();
      int lineLength = lineElem.getEndOffset() - lineStart;
      String line = doc.getText(lineStart, lineLength);

      // determine current indentation and number of whitespace characters
      int wsChars;
      int indentation = 0;
      for (wsChars = 0; wsChars < line.length(); wsChars++) {
        char c = line.charAt(wsChars);
        if (c == ' ') {
          indentation++;
        } else if (c == '\t') {
          indentation += tabWidth;
        } else {
          break;
        }
      }

      int newIndentation = indentation + columns;
      if (newIndentation <= 0) {
        doc.remove(lineStart, wsChars);
        return;
      }

      // build whitespace string for new indentation
      StringBuilder newWs = new StringBuilder(newIndentation / tabWidth + tabWidth - 1);
      int ind = 0;
      if (!expandTab) {
        for (; ind + tabWidth <= newIndentation; ind += tabWidth) {
          newWs.append('\t');
        }
      }
      for (; ind < newIndentation; ind++) {
        newWs.append(' ');
      }
      doc.replace(lineStart, wsChars, newWs.toString(), null);
    }
  }

  public static class CompletionAction extends TextAction {

    private Segment segLine;

    public CompletionAction() {
      this(completionAction);
    }

    public CompletionAction(String name) {
      super(name);
      segLine = new Segment();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      JTextComponent text = (JTextComponent) e.getSource();
      actionPerformed(text);
    }

    public void actionPerformed(JTextComponent text) {
      StyledDocument doc = (StyledDocument) text.getDocument();
      Caret c = text.getCaret();
      int dot = c.getDot();
      if (dot != c.getMark()) {
        return;
      }
      Element map = doc.getDefaultRootElement();
      int line = map.getElementIndex(dot);
      Element elem = map.getElement(line);
      int start = elem.getStartOffset();
      int end = dot;//elem.getEndOffset() - 1;
      try {
        doc.getText(start, end - start, segLine);
      } catch (BadLocationException e) {
        Debug.error("EditorPane: CompletionAction: BadLocationException %s", e.getMessage());
        return;
      }
      Debug.log(3, "EditorPane: CompletionAction: [%d] |%s| (%d)", line + 1, segLine, dot);
      if (popCompletion != null) {
        popCompletion.show(pane, 100, 100);
      }
    }
  }

  private static class NextVisualPositionAction extends TextAction {

    private boolean select;
    private int direction;
    private String command;

    NextVisualPositionAction(String nm, boolean select, int dir) {
//TODO forward selection space+image - space not selected alone
//TODO up/down might step left or right
      super(nm);

      this.select = select;
      this.direction = dir;
      this.command = nm;
    }

    private static int getNSVisualPosition(EditorPane txt, int pos, int direction) {
      Element root = txt.getDocument().getDefaultRootElement();
      int numLines = root.getElementIndex(txt.getDocument().getLength() - 1) + 1;
      int line = root.getElementIndex(pos) + 1;
      int tarLine = direction == SwingConstants.NORTH ? line - 1 : line + 1;
      try {
        if (tarLine <= 0) {
          return 0;
        }
        if (tarLine > numLines) {
          return txt.getDocument().getLength();
        }

        Rectangle curRect = txt.modelToView(pos);
        Rectangle tarEndRect;
        if (tarLine < numLines) {
          tarEndRect = txt.modelToView(txt.getLineStartOffset(tarLine) - 1);
        } else {
          tarEndRect = txt.modelToView(txt.getDocument().getLength() - 1);
        }
        Debug.log(9, "curRect: " + curRect + ", tarEnd: " + tarEndRect);

        if (curRect.x > tarEndRect.x) {
          pos = txt.viewToModel(new Point(tarEndRect.x, tarEndRect.y));
        } else {
          pos = txt.viewToModel(new Point(curRect.x, tarEndRect.y));
        }
      } catch (BadLocationException e) {
        Debug.error(me + "Problem getting next visual position\n%s", e.getMessage());
      }

      return pos;

    }

    @Override
    public void actionPerformed(ActionEvent e) {
      JTextComponent textArea = (JTextComponent) e.getSource();

      Caret caret = textArea.getCaret();
      int dot = caret.getDot();

      /*
       * Move to the beginning/end of selection on a "non-shifted"
       * left- or right-keypress.  We shouldn't have to worry about
       * navigation filters as, if one is being used, it let us get
       * to that position before.
       */
      if (!select) {
        switch (direction) {
          case SwingConstants.EAST:
            int mark = caret.getMark();
            if (dot != mark) {
              caret.setDot(Math.max(dot, mark));
              return;
            }
            break;
          case SwingConstants.WEST:
            mark = caret.getMark();
            if (dot != mark) {
              caret.setDot(Math.min(dot, mark));
              return;
            }
            break;
          default:
        }
      }

      Position.Bias[] bias = new Position.Bias[1];
      Point magicPosition = caret.getMagicCaretPosition();

      try {

        if (magicPosition == null
                && (direction == SwingConstants.NORTH
                || direction == SwingConstants.SOUTH)) {
          Rectangle r = textArea.modelToView(dot);
          magicPosition = new Point(r.x, r.y);
        }

        NavigationFilter filter = textArea.getNavigationFilter();

        if (filter != null) {
          dot = filter.getNextVisualPositionFrom(textArea, dot,
                  Position.Bias.Forward, direction, bias);
        } else {
          if (direction == SwingConstants.NORTH
                  || direction == SwingConstants.SOUTH) {
            dot = getNSVisualPosition((EditorPane) textArea, dot, direction);
          } else {
            dot = textArea.getUI().getNextVisualPositionFrom(
                    textArea, dot,
                    Position.Bias.Forward, direction, bias);
          }
        }
        if (select) {
          caret.moveDot(dot);
        } else {
          caret.setDot(dot);
        }

        if (magicPosition != null
                && (direction == SwingConstants.NORTH
                || direction == SwingConstants.SOUTH)) {
          caret.setMagicCaretPosition(magicPosition);
        }

      } catch (BadLocationException ble) {
        Debug.error(me + "Problem while trying to move caret\n%s", ble.getMessage());
      }

    }
  }
  

  private static class NextVisualLineAction extends TextAction {

    private boolean select;
    private int direction;
    private String command;

    NextVisualLineAction(String command, boolean select, int dir) {
      super(command);

      this.select = select;
      this.direction = dir;
      this.command = command;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      JTextComponent textArea = (JTextComponent) e.getSource();

      Caret caret = textArea.getCaret();
      int dot = caret.getDot();
      EditorPane currentCodePane = SikulixIDE.get().getCurrentCodePane();
      int lineNumberAtCaret = currentCodePane.getLineNumberAtCaret(dot);

      Element line = currentCodePane.getLineAtCaret(dot);
      int count = line.getElementCount();

      if (!select) {
        switch (direction) {
          case SwingConstants.EAST:
            currentCodePane.jumpTo(lineNumberAtCaret + 1);
            caret.setDot(caret.getDot() - 1);
            return;
          case SwingConstants.WEST:
            currentCodePane.jumpTo(lineNumberAtCaret);
            return;
          default:
        }
      } else {
        switch (direction) {
          case SwingConstants.EAST:
            try {
              int startOffset = currentCodePane.getLineStartOffset(lineNumberAtCaret);
              caret.moveDot(startOffset - 1);
            } catch (BadLocationException e1) {
            }
            return;
          case SwingConstants.WEST:
            try {
              int startOffset = currentCodePane.getLineStartOffset(lineNumberAtCaret - 1);
              caret.moveDot(startOffset);
            } catch (BadLocationException e1) {
            }
            return;
          default:
        }
      }
    }
  }

  //<editor-fold defaultstate="collapsed" desc="general support functions">
  @Override
  public Action[] getActions() {
    return TextAction.augmentList(super.getActions(), defaultActions);
  }

  @Override
  public ViewFactory getViewFactory() {
    return _viewFactory;
  }

  @Override
  public String getContentType() {
    return pane.getType();
  }

  @Override
  public void read(Reader in, Document doc, int pos)
          throws IOException, BadLocationException {
    Debug.log(3, "SikuliEditorKit.read");
    super.read(in, doc, pos);
  }

  @Override
  public void write(Writer out, Document doc, int pos, int len)
          throws IOException, BadLocationException {
    write(out, doc, pos, len, null);
  }

  public void write(Writer out, Document doc, int pos, int len, Map<String, String> copiedImgs)
          throws IOException, BadLocationException {
    Debug.log(9, "SikuliEditorKit.write %d %d", pos, len);
    DefaultStyledDocument sdoc = (DefaultStyledDocument) doc;
    int i = pos;
    while (i < pos + len) {
      Element element = sdoc.getCharacterElement(i);
      int start = element.getStartOffset(), end = element.getEndOffset();
      if (element.getName().equals(StyleConstants.ComponentElementName)) {
        AttributeSet attr = element.getAttributes();
        Component comp = StyleConstants.getComponent(attr);
        out.write(comp.toString());
        if (copiedImgs != null && comp instanceof EditorImageButton) {
          final String absPath = ((EditorImageButton) comp).getFilename();
          String fname = (new File(absPath)).getName();
          copiedImgs.put(fname, absPath);
          Debug.log(3, "SikuliEditorKit: image: %s",absPath);
        }
      } else {
        if (start < pos) {
          start = pos;
        }
        if (end > pos + len) {
          end = pos + len;
        }
        out.write(doc.getText(start, end - start));
      }
      i = end;
    }
    out.close();
  }
//</editor-fold>

}
