/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2004-10 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

package processing.mode.java;

import processing.app.Editor;
import processing.app.Preferences;
import processing.app.Sketch;
import processing.app.syntax.*;

import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;


/**
 * Filters key events for tab expansion/indent/etc.
 * <p/>
 * For version 0099, some changes have been made to make the indents
 * smarter. There are still issues though:
 * <UL>
 * <LI> indent happens when it picks up a curly brace on the previous line,
 *   but not if there's a  blank line between them.
 * <LI> It also doesn't handle single indent situations where a brace
 *   isn't used (i.e. an if statement or for loop that's a single line).
 *   It shouldn't actually be using braces.
 * </UL>
 * Solving these issues, however, would probably best be done by a
 * smarter parser/formatter, rather than continuing to hack this class.
 */
public class PdeKeyListener {
  private Editor editor;
  private JEditTextArea textarea;

  /** ctrl-alt on windows and linux, cmd-alt on mac os x */
  static final int CTRL_ALT = ActionEvent.ALT_MASK |
    Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();


  public PdeKeyListener(Editor editor, JEditTextArea textarea) {
    this.editor = editor;
    this.textarea = textarea;

    // let him know that i'm leechin'
    textarea.editorListener = this;
  }


  /**
   * Intercepts key pressed events for JEditTextArea.
   * <p/>
   * Called by JEditTextArea inside processKeyEvent(). Note that this
   * won't intercept actual characters, because those are fired on
   * keyTyped().
   * @return true if the event has been handled (to remove it from the queue)
   */
  public boolean keyPressed(KeyEvent event) {
    char c = event.getKeyChar();
    int code = event.getKeyCode();

    Sketch sketch = editor.getSketch();

    if ((event.getModifiers() & CTRL_ALT) == CTRL_ALT) {
      if (code == KeyEvent.VK_LEFT) {
        sketch.handlePrevCode();
        return true;
      } else if (code == KeyEvent.VK_RIGHT) {
        sketch.handleNextCode();
        return true;
      }
    }

    if ((event.getModifiers() & InputEvent.META_MASK) != 0) {
      //event.consume();  // does nothing
      return false;
    }

    // TODO i don't like these accessors. clean em up later.
//    if (!editor.getSketch().isModified()) {
    if ((code == KeyEvent.VK_BACK_SPACE) || (code == KeyEvent.VK_TAB) ||
        (code == KeyEvent.VK_ENTER) || ((c >= 32) && (c < 128))) {
      sketch.setModified(true);
    }
//    }

    if ((code == KeyEvent.VK_UP) &&
        ((event.getModifiers() & InputEvent.CTRL_MASK) != 0)) {
      // back up to the last empty line
      char contents[] = textarea.getText().toCharArray();
      //int origIndex = textarea.getCaretPosition() - 1;
      int caretIndex = textarea.getCaretPosition();

      int index = calcLineStart(caretIndex - 1, contents);
      //System.out.println("line start " + (int) contents[index]);
      index -= 2;  // step over the newline
      //System.out.println((int) contents[index]);
      boolean onlySpaces = true;
      while (index > 0) {
        if (contents[index] == 10) {
          if (onlySpaces) {
            index++;
            break;
          } else {
            onlySpaces = true;  // reset
          }
        } else if (contents[index] != ' ') {
          onlySpaces = false;
        }
        index--;
      }
      // if the first char, index will be -2
      if (index < 0) index = 0;

      if ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
        textarea.setSelectionStart(caretIndex);
        textarea.setSelectionEnd(index);
      } else {
        textarea.setCaretPosition(index);
      }
      event.consume();
      return true;

    } else if ((code == KeyEvent.VK_DOWN) &&
               ((event.getModifiers() & InputEvent.CTRL_MASK) != 0)) {
      char contents[] = textarea.getText().toCharArray();
      int caretIndex = textarea.getCaretPosition();

      int index = caretIndex;
      int lineStart = 0;
      boolean onlySpaces = false;  // don't count this line
      while (index < contents.length) {
        if (contents[index] == 10) {
          if (onlySpaces) {
            index = lineStart;  // this is it
            break;
          } else {
            lineStart = index + 1;
            onlySpaces = true;  // reset
          }
        } else if (contents[index] != ' ') {
          onlySpaces = false;
        }
        index++;
      }
      // if the first char, index will be -2
      //if (index < 0) index = 0;

      //textarea.setSelectionStart(index);
      //textarea.setSelectionEnd(index);
      if ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
        textarea.setSelectionStart(caretIndex);
        textarea.setSelectionEnd(index);
      } else {
        textarea.setCaretPosition(index);
      }
      event.consume();
      return true;
    }


    switch (c) {

    case 9:  // TAB
      if ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
        // if shift is down, the user always expects an outdent
        // http://code.google.com/p/processing/issues/detail?id=458
        editor.handleOutdent();

      } else if (textarea.isSelectionActive()) {
        editor.handleIndent();

      } else if (Preferences.getBoolean("editor.tabs.expand")) {
        int tabSize = Preferences.getInteger("editor.tabs.size");
        textarea.setSelectedText(spaces(tabSize));
        event.consume();
        return true;
      }
      break;

    case 10:  // auto-indent
    case 13:
      if (Preferences.getBoolean("editor.indent")) {
        char contents[] = textarea.getText().toCharArray();
        int tabSize = Preferences.getInteger("editor.tabs.size");

        // this is the previous character
        // (i.e. when you hit return, it'll be the last character
        // just before where the newline will be inserted)
        int origIndex = textarea.getCaretPosition() - 1;

        // NOTE all this cursing about CRLF stuff is probably moot
        // NOTE since the switch to JEditTextArea, which seems to use
        // NOTE only LFs internally (thank god). disabling for 0099.
        // walk through the array to the current caret position,
        // and count how many weirdo windows line endings there are,
        // which would be throwing off the caret position number
        /*
        int offset = 0;
        int realIndex = origIndex;
        for (int i = 0; i < realIndex-1; i++) {
          if ((contents[i] == 13) && (contents[i+1] == 10)) {
            offset++;
            realIndex++;
          }
        }
        // back up until \r \r\n or \n.. @#($* cross platform
        //System.out.println(origIndex + " offset = " + offset);
        origIndex += offset; // ARGH!#(* WINDOWS#@($*
        */

        // if the previous thing is a brace (whether prev line or
        // up farther) then the correct indent is the number of spaces
        // on that line + 'indent'.
        // if the previous line is not a brace, then just use the
        // identical indentation to the previous line

        // calculate the amount of indent on the previous line
        // this will be used *only if the prev line is not an indent*
        int spaceCount = calcSpaceCount(origIndex, contents);

        // If the last character was a left curly brace, then indent.
        // For 0122, walk backwards a bit to make sure that the there
        // isn't a curly brace several spaces (or lines) back. Also
        // moved this before calculating extraCount, since it'll affect
        // that as well.
        int index2 = origIndex;
        while ((index2 >= 0) &&
               Character.isWhitespace(contents[index2])) {
          index2--;
        }
        if (index2 != -1) {
          // still won't catch a case where prev stuff is a comment
          if (contents[index2] == '{') {
            // intermediate lines be damned,
            // use the indent for this line instead
            spaceCount = calcSpaceCount(index2, contents);
            spaceCount += tabSize;
          }
        }
        //System.out.println("spaceCount should be " + spaceCount);

        // now before inserting this many spaces, walk forward from
        // the caret position and count the number of spaces,
        // so that the number of spaces aren't duplicated again
        int index = origIndex + 1;
        int extraCount = 0;
        while ((index < contents.length) &&
               (contents[index] == ' ')) {
          //spaceCount--;
          extraCount++;
          index++;
        }
        int braceCount = 0;
        while ((index < contents.length) && (contents[index] != '\n')) {
          if (contents[index] == '}') {
            braceCount++;
          }
          index++;
        }

        // hitting return on a line with spaces *after* the caret
        // can cause trouble. for 0099, was ignoring the case, but this is
        // annoying, so in 0122 we're trying to fix that.
        /*
        if (spaceCount - extraCount > 0) {
          spaceCount -= extraCount;
        }
        */
        spaceCount -= extraCount;
        //if (spaceCount < 0) spaceCount = 0;
        //System.out.println("extraCount is " + extraCount);

        // now, check to see if the current line contains a } and if so,
        // outdent again by indent
        //if (braceCount > 0) {
        //spaceCount -= 2;
        //}

        if (spaceCount < 0) {
          // for rev 0122, actually delete extra space
          //textarea.setSelectionStart(origIndex + 1);
          textarea.setSelectionEnd(textarea.getSelectionStop() - spaceCount);
          textarea.setSelectedText("\n");
        } else {
          String insertion = "\n" + spaces(spaceCount);
          textarea.setSelectedText(insertion);
        }

        // not gonna bother handling more than one brace
        if (braceCount > 0) {
          int sel = textarea.getSelectionStart();
          // sel - tabSize will be -1 if start/end parens on the same line
          // http://dev.processing.org/bugs/show_bug.cgi?id=484
          if (sel - tabSize >= 0) {
            textarea.select(sel - tabSize, sel);
            String s = spaces(tabSize);
            // if these are spaces that we can delete
            if (textarea.getSelectedText().equals(s)) {
              textarea.setSelectedText("");
            } else {
              textarea.select(sel, sel);
            }
          }
        }
      } else {
        // Enter/Return was being consumed by somehow even if false
        // was returned, so this is a band-aid to simply fire the event again.
        // http://dev.processing.org/bugs/show_bug.cgi?id=1073
        textarea.setSelectedText(String.valueOf(c));
      }
      // mark this event as already handled (all but ignored)
      event.consume();
      return true;

    case '}':
      if (Preferences.getBoolean("editor.indent")) {
        // first remove anything that was there (in case this multiple
        // characters are selected, so that it's not in the way of the
        // spaces for the auto-indent
        if (textarea.getSelectionStart() != textarea.getSelectionStop()) {
          textarea.setSelectedText("");
        }

        // if this brace is the only thing on the line, outdent
        char contents[] = textarea.getText().toCharArray();
        // index to the character to the left of the caret
        int prevCharIndex = textarea.getCaretPosition() - 1;

        // backup from the current caret position to the last newline,
        // checking for anything besides whitespace along the way.
        // if there's something besides whitespace, exit without
        // messing any sort of indenting.
        int index = prevCharIndex;
        boolean finished = false;
        while ((index != -1) && (!finished)) {
          if (contents[index] == 10) {
            finished = true;
            index++;
          } else if (contents[index] != ' ') {
            // don't do anything, this line has other stuff on it
            return false;
          } else {
            index--;
          }
        }
        if (!finished) return false;  // brace with no start
        int lineStartIndex = index;

        int pairedSpaceCount = calcBraceIndent(prevCharIndex, contents); //, 1);
        if (pairedSpaceCount == -1) return false;

        textarea.setSelectionStart(lineStartIndex);
        textarea.setSelectedText(spaces(pairedSpaceCount));

        // mark this event as already handled
        event.consume();
        return true;
      }
      break;
    }
    return false;
  }


  public boolean keyTyped(KeyEvent event) {
    char c = event.getKeyChar();

    if ((event.getModifiers() & InputEvent.CTRL_MASK) != 0) {
      // on linux, ctrl-comma (prefs) being passed through to the editor
      if (c == KeyEvent.VK_COMMA) {
        event.consume();
        return true;
      }
    }
    return false;
  }



  /**
   * Return the index for the first character on this line.
   */
  protected int calcLineStart(int index, char contents[]) {
    // backup from the current caret position to the last newline,
    // so that we can figure out how far this line was indented
    /*int spaceCount = 0;*/
    boolean finished = false;
    while ((index != -1) && (!finished)) {
      if ((contents[index] == 10) ||
          (contents[index] == 13)) {
        finished = true;
        //index++; // maybe ?
      } else {
        index--;  // new
      }
    }
    // add one because index is either -1 (the start of the document)
    // or it's the newline character for the previous line
    return index + 1;
  }


  /**
   * Calculate the number of spaces on this line.
   */
  protected int calcSpaceCount(int index, char contents[]) {
    index = calcLineStart(index, contents);

    int spaceCount = 0;
    // now walk forward and figure out how many spaces there are
    while ((index < contents.length) && (index >= 0) &&
           (contents[index++] == ' ')) {
      spaceCount++;
    }
    return spaceCount;
  }


  /**
   * Walk back from 'index' until the brace that seems to be
   * the beginning of the current block, and return the number of
   * spaces found on that line.
   */
  protected int calcBraceIndent(int index, char contents[]) {
    // now that we know things are ok to be indented, walk
    // backwards to the last { to see how far its line is indented.
    // this isn't perfect cuz it'll pick up commented areas,
    // but that's not really a big deal and can be fixed when
    // this is all given a more complete (proper) solution.
    int braceDepth = 1;
    boolean finished = false;
    while ((index != -1) && (!finished)) {
      if (contents[index] == '}') {
        // aww crap, this means we're one deeper
        // and will have to find one more extra {
        braceDepth++;
        //if (braceDepth == 0) {
        //finished = true;
        //}
        index--;
      } else if (contents[index] == '{') {
        braceDepth--;
        if (braceDepth == 0) {
          finished = true;
        }
        index--;
      } else {
        index--;
      }
    }
    // never found a proper brace, be safe and don't do anything
    if (!finished) return -1;

    // check how many spaces on the line with the matching open brace
    //int pairedSpaceCount = calcSpaceCount(index, contents);
    //System.out.println(pairedSpaceCount);
    return calcSpaceCount(index, contents);
  }


//  /**
//   * Get the character array and blank out the commented areas.
//   * This hasn't yet been tested, the plan was to make auto-indent
//   * less gullible (it gets fooled by braces that are commented out).
//   */
//  protected char[] getCleanedContents() {
//    char c[] = textarea.getText().toCharArray();
//
//    int index = 0;
//    while (index < c.length - 1) {
//      if ((c[index] == '/') && (c[index+1] == '*')) {
//        c[index++] = 0;
//        c[index++] = 0;
//        while ((index < c.length - 1) &&
//               !((c[index] == '*') && (c[index+1] == '/'))) {
//          c[index++] = 0;
//        }
//
//      } else if ((c[index] == '/') && (c[index+1] == '/')) {
//        // clear out until the end of the line
//        while ((index < c.length) && (c[index] != 10)) {
//          c[index++] = 0;
//        }
//        if (index != c.length) {
//          index++;  // skip over the newline
//        }
//      }
//    }
//    return c;
//  }


  static String spaces(int count) {
    char[] c = new char[count];
    Arrays.fill(c, ' ');
    return new String(c);
  }
}
