package org.basex.gui.layout;

import java.util.Arrays;
import org.basex.util.TokenBuilder;
import static org.basex.util.Token.*;

/**
 * This class allows the iteration on tokens.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 */
public final class BaseXTextTokens {
  /** Tab width. */
  static final int TAB = 2;
  /** Text array to be written. */
  byte[] text = EMPTY;

  /** Text length. */
  private int size;
  /** Current start position. */
  private int ps;
  /** Current end position. */
  private int pe;
  /** Current cursor position. */
  private int pc;
  /** Start of a text mark. */
  private int ms = -1;
  /** End of a text mark. */
  private int me = -1;
  /** Start of an error mark. */
  private int es = -1;

  /**
   * Constructor.
   * @param t text
   */
  BaseXTextTokens(final byte[] t) {
    this(t, t.length);
  }

  /**
   * Constructor.
   * @param t text
   * @param s buffer size
   */
  BaseXTextTokens(final byte[] t, final int s) {
    text = t;
    size = s;
  }

  /**
   * Initializes the iterator.
   */
  void init() {
    ps = 0;
    pe = 0;
  }

  /**
   * Checks if the text contains more words.
   * @return result of check
   */
  boolean moreWords() {
    // quit if text has ended
    if(pe >= size) return false;
    ps = pe;

    // find next token boundary
    int ch = cp(text, ps);
    pe += cl(text, ps);
    if(!ftChar(ch)) return true;

    while(pe < size) {
      ch = cp(text, pe);
      if(!ftChar(ch)) break;
      pe += cl(text, pe);
    }
    return true;
  }

  /**
   * Returns the token as word.
   * @return word
   */
  public String nextWord() {
    return string(text, ps, pe - ps);
  }

  /**
   * Returns the length of the current word.
   * @return length
   */
  int length() {
    return pe - ps;
  }

  /**
   * Moves one character forward.
   * @param mark mark flag
   * @return character
   */
  int next(final boolean mark) {
    return noMark(mark, true) ? curr() : next();
  }

  /**
   * Moves one token forward.
   * @param mark mark flag
   */
  void nextToken(final boolean mark) {
    int ch = next(mark);
    if(ch == '\n') return;
    if(Character.isLetterOrDigit(ch)) {
      while(Character.isLetterOrDigit(ch)) ch = next();
      while(ch != '\n' && Character.isWhitespace(ch)) ch = next();
    } else if(Character.isWhitespace(ch)) {
      while(ch != '\n' && Character.isWhitespace(ch)) ch = next();
    } else {
      while(ch != '\n' && !Character.isLetterOrDigit(ch) &&
          !Character.isWhitespace(ch)) ch = next();
      while(ch != '\n' && Character.isWhitespace(ch)) ch = next();
    }
    if(ps != size) prev();
  }

  /**
   * Moves one token back.
   * @param mark mark flag
   */
  void prevToken(final boolean mark) {
    int ch = prev(mark);
    if(ch == '\n') return;
    if(Character.isLetterOrDigit(ch)) {
      while(Character.isLetterOrDigit(ch)) ch = prev();
    } else if(Character.isWhitespace(ch)) {
      while(ch != '\n' && Character.isWhitespace(ch)) ch = prev();
      while(Character.isLetterOrDigit(ch)) ch = prev();
    } else {
      while(ch != '\n' && !Character.isLetterOrDigit(ch) &&
          !Character.isWhitespace(ch)) ch = prev();
    }
    if(ps != 0) next();
  }

  /**
   * Checks if the character position equals the word end.
   * @return result of check
   */
  boolean more() {
    return ps < pe;
  }

  /**
   * Returns the current character.
   * @return current character
   */
  public int curr() {
    return ps >= size ? '\n' : cp(text, ps);
  }

  /**
   * Moves one character forward.
   * @return character
   */
  int next() {
    final int c = curr();
    if(ps < size) ps += cl(text, ps);
    return c;
  }

  /**
   * Sets the iterator position.
   * @param p iterator position
   */
  void pos(final int p) {
    ps = p;
  }

  /**
   * Returns the iterator position.
   * @return iterator position
   */
  int pos() {
    return ps;
  }

  /**
   * Returns the byte array, chopping the unused bytes.
   * @return character array
   */
  byte[] toArray() {
    return text.length == size ? text : Arrays.copyOf(text, size);
  }

  // POSITION =================================================================

  /**
   * Moves to the beginning of the line.
   * @param mark mark flag
   * @return number of moved characters
   */
  int bol(final boolean mark) {
    int c = 0;
    if(ps == 0) return 0;
    do c += curr() == '\t' ? TAB : 1; while(prev(mark) != '\n');
    if(ps != 0 || curr() == '\n') next(mark);
    return c;
  }

  /**
   * Moves one character back and returns the found character.
   * @param mark mark flag
   * @return character
   */
  int prev(final boolean mark) {
    return noMark(mark, false) ? curr() : prev();
  }

  /**
   * Moves one character back and returns the found character.
   * @return character
   */
  int prev() {
    if(ps == 0) return '\n';
    final int p = ps;
    ps = Math.max(0, ps - 5);
    while(ps < p && ps + cl(text, ps) < p) ++ps;
    return curr();
  }

  /**
   * Moves to the specified position of to the of the line.
   * @param p position to move to
   * @param mark mark flag
   */
  void forward(final int p, final boolean mark) {
    int nc = 0;
    while(curr() != '\n') {
      if((nc += curr() == '\t' ? TAB : 1) >= p) return;
      next(mark);
    }
  }

  /**
   * Adds a string at the current position.
   * @param str string
   */
  void add(final String str) {
    final TokenBuilder tb = new TokenBuilder();
    tb.add(text, 0, ps);
    int cc = 0;
    final int cl = str.length();
    for(int c = 0; c < cl; ++c) {
      // ignore invalid characters
      int ch = str.charAt(c);
      if(ch == '\r') continue;
      if(ch < ' ' && !ws(ch)) ch = '\n';
      tb.add(ch);
      ++cc;
    }
    tb.add(text, ps, size);
    text = tb.finish();
    size = tb.size();
    for(int c = 0; c < cc; ++c) next();
  }

  /**
   * Indents lines.
   * @param s start position
   * @param e end position
   * @param sh shift flag
   */
  void indent(final int s, final int e, final boolean sh) {
    // extend selection to match whole lines
    pos(s);
    bol(true);
    startMark();
    pos(e);
    forward(Integer.MAX_VALUE, true);
    next(true);
    endMark();

    // decide if to use tab or spaces
    boolean tab = false;
    for(int p = 0; p < size; ++p) tab |= text[p] == '\t';
    byte[] add = { '\t' };
    if(!tab) {
      add = new byte[TAB];
      for(int a = 0; a < TAB; a++) add[a] = ' ';
    }

    // build new text
    final TokenBuilder tb = new TokenBuilder();
    tb.add(text, 0, ms);
    for(int p = ms; p < ps; p += cl(text, p)) {
      if(p == 0 || text[p - 1] == '\n') {
        if(sh) {
          // remove indentation
          if(text[p] == '\t') {
            me--;
            continue;
          }
          if(text[p] == ' ') {
            me--;
            for(int i = 1; i < TAB && p + i < size && text[p + i] == ' '; i++) {
              me--;
              p++;
            }
            continue;
          }
        } else {
          // add new indentation
          tb.add(add);
          me += add.length;
        }
      }
      tb.add(cp(text, p));
    }
    tb.add(text, ps, size);
    ps = me;
    text = tb.finish();
    size = tb.size();
  }


  /**
   * (Un)comments highlighted text or line.
   * @param syntax syntax highlighter
   */
  void comment(final BaseXSyntax syntax) {
    final byte[] start = syntax.commentOpen();
    final byte[] end = syntax.commentEnd();
    boolean add = true;
    int min = ps;
    int max = ps;

    if(marked()) {
      min = ps < ms ? ps : ms;
      max = ps > ms ? ps : ms;
      // marked
      final int mn = Math.max(min + start.length, max - end.length);
      if(indexOf(text, start, min) == min &&
         indexOf(text, end, mn) == mn) {
        final TokenBuilder tb = new TokenBuilder();
        tb.add(text, 0, min);
        tb.add(text, min + start.length, max - end.length);
        tb.add(text, max, size);
        text = tb.finish();
        size = tb.size();
        ms = min;
        me = max - start.length - end.length;
        ps = me;
        add = false;
      }
    } else {
      while(min > 0 && text[min - 1] != '\n') --min;
      while(max < size() && text[max] != '\n') ++max;
    }

    if(add) {
      pos(max);
      add(string(end));
      pos(min);
      add(string(start));
      ms = min;
      me = max + start.length + end.length;
      ps = me;
    }
  }

  /**
   * Deletes the current character.
   * Assumes that the current position allows a deletion.
   */
  void delete() {
    if(size == 0) return;
    final TokenBuilder tb = new TokenBuilder();
    final int s = marked() ? Math.min(ms, me) : ps;
    final int e = marked() ? Math.max(ms, me) : ps + cl(text, ps);
    tb.add(text, 0, s);
    if(e < size) tb.add(text, e, size);
    text = tb.finish();
    size = tb.size();
    ps = s;
    noMark();
  }

  // TEXT MARKING =============================================================

  /**
   * Jumps to the maximum/minimum position and resets the selection.
   * @param mark marking flag
   * @param max maximum/minimum flag
   * @return true if mark was reset
   */
  private boolean noMark(final boolean mark, final boolean max) {
    final boolean rs = !mark && marked();
    if(rs) {
      ps = max ^ ms < me ? ms : me;
      noMark();
    }
    return rs;
  }

  /**
   * Resets the selection.
   */
  void noMark() {
    ms = -1;
    me = -1;
  }

  /**
   * Sets the start of a text mark.
   */
  void startMark() {
    ms = ps;
    me = ps;
  }

  /**
   * Sets the end of a text mark.
   */
  void endMark() {
    me = ps;
  }

  /**
   * Returns the start of the text mark. The value is {@code -1} if no
   * text is selected.
   * @return start mark
   */
  int start() {
    return ms;
  }

  /**
   * Tests if some text is marked.
   * @return result of check
   */
  boolean marked() {
    return ms != -1;
  }

  /**
   * Checks the validity of the mark.
   */
  void checkMark() {
    if(ms == me) noMark();
  }

  /**
   * Tests if the current position is marked.
   * @return result of check
   */
  boolean markStart() {
    return marked() &&
        (inMark() || (ms < me ? ms >= ps && ms < pe : me >= ps && me < pe));
  }

  /**
   * Tests if the current position is marked.
   * @return result of check
   */
  boolean inMark() {
    return ms < me ? ps >= ms && ps < me : ps >= me && ps < ms;
  }

  /**
   * Returns the marked substring.
   * @return substring
   */
  String copy() {
    if(!marked()) return "";
    final TokenBuilder tb = new TokenBuilder();
    final int e = ms < me ? me : ms;
    for(int s = ms < me ? ms : me; s < e; s += cl(text, s)) {
      final int t = cp(text, s);
      if(t < 0 || t >= ' ' || t == 0x0A || t == 0x09) tb.add(t);
    }
    return tb.toString();
  }

  // ERROR MARK ===============================================================

  /**
   * Tests if the current token is erroneous.
   * @return result of check
   */
  boolean error() {
    return es >= ps && es <= pe;
  }

  /**
   * Tests if the cursor moves over the current token.
   * @param s start
   */
  void error(final int s) {
    es = s;
  }

  // CURSOR ===================================================================

  /**
   * Checks if the cursor moves over the current token.
   * @return result of check
   */
  boolean edited() {
    return pc >= ps && pc <= pe;
  }

  /**
   * Sets the caret position to the specified counter.
   * @param c caret position
   */
  void setCaret(final int c) {
    pc = c;
  }

  /**
   * Sets the caret position to the current counter.
   */
  void setCaret() {
    pc = ps;
  }

  /**
   * Returns the cursor position.
   * @return cursor position
   */
  int cursor() {
    return pc;
  }

  /**
   * Returns the text size.
   * @return text size
   */
  int size() {
    return size;
  }

  @Override
  public String toString() {
    return copy();
  }
}
