/*
 * StringSequence class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.text;

import java.util.Collection;

/**
 * 文字列をコード・ポイント単位に順次アクセスするためのクラス。
 * <br>
 * Unicodeの補助文字の対応(JSR-204)により、文字と<code>char</code>変数が
 * 必ずしも対応しなくなり、文字列操作におけるインデックスの扱いが複雑に
 * なった。このクラスは、そのインデックスの扱いを簡単にするための仕組みを
 * {@link ts.util.text.StringOperation StringOperation}とともに用意する。
 * <br>
 * 文字列中のインデックスを、コード・ポイントごとに進めたり戻したりして、
 * そのインデックスに位置する文字を取得したり、そのインデックスから開始する
 * 部分文字列を取得したりすることができる。
 * <br>
 * インデックスの取り得る値の範囲は、<tt>-1〜n</tt> （n はコード・ポイント単位
 * の文字列長）であり、インデックスが<tt>-1</tt>又は<tt>n</tt>の場合は、それに
 * 位置する文字として空文字列を返す。
 * <br>
 * <br>
 * 先頭から文字を順番に取得する方法は、以下のように行うことができる：<code><pre>
 *     for (StringSequence seq = new StringSequence(aStr);
 *         seq.validIndex(); seq.next()) {
 *       String chStr = seq.character();     // 文字を順に取得。
 *       String subStr = seq.substring();    // 現在のインデックスから開始する部分文字列を取得。
 *       ...
 *     }
 *
 *     あるいは
 *
 *     StringSequence seq = new StringSequence(aStr, -1);
 *     while (seq.hasNext()) {
 *       seq.next();
 *       String ch = seq.character();      // 文字を順に取得。
 *       String subStr = seq.substring();  // 現在のインデックスから開始する部分文字列を取得。
 *       ...
 *     } 
 *
 *     あるいは
 *
 *     if (! StringOperation.isEmpty(aStr)) {
 *       StringSequence seq = new StringSequence(aStr, -1);
 *       do {
 *         String chStr = StringOperation.characterAt(seq.index() + 1); // 文字を順に取得。
 *         String subStr = seq.followingString();  // 現在のインデックスの次から開始する部分文字列を取得。
 *         ...
 *       } while (seq.next().validIndex());
 *     }</pre></code>
 * 上記の例は、いずれも同じ文字や文字列の値が得られる。
 * 
 * @author  佐藤隆之.
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:53 $
 */
public final class StringSequence implements Cloneable
{
  /** 操作対象の文字列。 */
  private final String str_ ;

  /** 操作対象の文字列のコード・ポイント数。 */
  private final int codePointCount_ ;

  /** <code>char</code>インデックス。 */
  private int charIndex_ ;

  /** コード・ポイント・インデックス。 */
  private int codePointIndex_ ;

  /**
   * 操作対象の文字列を引数にとるコンストラクタ。
   *
   * @param  str 操作対象の文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public StringSequence(String str)
  {
    assert (str != null) : "@param:str is null.";
    
    str_ = str;
    codePointCount_ = str.codePointCount(0, str.length());
    charIndex_ = 0;
    codePointIndex_ = 0;
  }

  /**
   * 操作対象の文字列と開始インデックスを引数にとるコンストラクタ。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の先頭からの順番
   * である。インデックスの取り得る範囲は、<tt>-1〜n</tt>（nはコード・ポイント
   * 単位の文字列長）である。
   *
   * @param  str 操作対象の文字列。
   * @param  beginCodePointIndex 開始インデックス。
   * @throws IndexOutOfBoundsException 開始インデックスが範囲外の場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public StringSequence(String str, int beginCodePointIndex)
    throws IndexOutOfBoundsException
  {
    assert (str != null) : "@param:str is null.";

    int ind;
    if (beginCodePointIndex == -1) {
      ind = -1;
    }
    else {
      ind = StringOperation.offsetByCodePoints(str, 0, beginCodePointIndex);
    }

    str_ = str;
    codePointCount_ = str.codePointCount(0, str.length());
    charIndex_ = ind;
    codePointIndex_ = beginCodePointIndex;
  }

  /**
   * コピー・コンストラクタ。
   *
   * @param seq {@link ts.util.text.StringSequence StringSequence}オブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  protected StringSequence(StringSequence seq)
  {
    assert (seq != null) : "@param:seq is null.";

    this.str_ = seq.str_;
    this.codePointCount_ = seq.codePointCount_ ;
    this.charIndex_ = seq.charIndex_;
    this.codePointIndex_ = seq.codePointIndex_;
  }

  /**
   * このオブジェクトのコピーを作成する。
   *
   * @return このオブジェクトのコピー。
   */
  public StringSequence copy()
  {
    return new StringSequence(this);
  }

  /**
   * 文字列の内容が等しいかどうかを確認する。
   * <br>
   * 現在のインデックスから開始する部分文字列の値を比較して、それが等しい場合
   * は<tt>true</tt>を、等しくない場合は<tt>false</tt>を返す。
   * <br>
   * 引数が {@link ts.util.text.StringSequence StringSequence}オブジェクトで
   * ない場合は常に<tt>false</tt>を返す。
   *
   * @param  obj 比較を行う{@link ts.util.text.StringSequence StringSequence}
   *           オブジェクト。
   * @return 現在のインデックスによる値が等しい場合は<tt>true</tt>を返す。
   */
  public boolean equals(Object obj)
  {
    if (obj != null && obj instanceof StringSequence) {
      return substring().equals(StringSequence.class.cast(obj).substring());
    }

    return false;
  }

  /**
   * このオブジェクトのハッシュ・コード値を取得する。
   *
   * @return ハッシュ・コード値。
   */
  public int hashCode()
  {
    return substring().hashCode();
  }

  /**
   * 現在のインデックスが範囲内（0〜n-1、nはコード・ポイント単位の文字列長）
   * にあるかどうかを確認する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の値をいう。
   *
   * @return 現在のインデックスが範囲内にある場合は<tt>true</tt>を返す。
   */
  public boolean validIndex()
  {
    return (0 <= charIndex_ && charIndex_ < str_.length()) ? true : false;
  }

  /**
   * 後続の文字が存在するかどうかを確認する。
   *
   * @return 後続の文字が存在すれば<tt>true</tt>を返す。
   */
  public boolean hasNext()
  {
    return (codePointIndex_ < codePointCount_ - 1);
  }

  /**
   * 前に文字が存在するかどうかを確認する。
   *
   * @return 前に文字が存在すれば<tt>true</tt>を返す。
   */
  public boolean hasPrevious()
  {
    return (codePointIndex_ > 0);
  }

  /**
   * インデックスを、コード・ポイント１つ分次に進める。
   * <br>
   * インデックスの取り得る範囲は-1〜n（nはコード・ポイント単位の文字列長）で
   * あり、次に進めるとこの範囲から外れる場合は、インデックスを移動しない。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の値をいう。
   *
   * @return インデックスを移動した後のこのオブジェクト。
   */
  public StringSequence next()
  {
    return next(1);
  }

  /**
   * インデックスを、指定された文字数分だけ次に進める。
   * <br>
   * 引数に負の値を指定した場合は、指定された文字数分前に戻す。
   * <br>
   * インデックスの取り得る範囲は-1〜n（nはコード・ポイント単位の文字列長）で
   * あり、次に進めるとこの範囲から外れる場合は、-1又はnにリミットをかける。
   * <br>
   * なお、ここでいうインデックスや文字数は、コード・ポイント単位の値をいう。
   *
   * @param  codePointCount 次に進める文字数。
   * @return インデックスを移動した後のこのオブジェクト。
   */
  public StringSequence next(int codePointCount)
  {
    if (codePointCount > 0) {
      _next(codePointCount);
    }
    else if (codePointCount < 0){
      _prev(-codePointCount);
    }

    return this;
  }

  /**
   * インデックスをコード・ポイント１つ分前に戻す。
   * <br>
   * インデックスの取り得る範囲は-1〜n（nはコード・ポイント単位の文字列長）で
   * あり、前に戻すとこの範囲から外れる場合は、インデックスを移動しない。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の値をいう。
   *
   * @return インデックスを移動した後のこのオブジェクト。
   */
  public StringSequence previous()
  {
    return previous(1);
  }

  /**
   * インデックスを、指定された文字数分だけ前に戻す。
   * <br>
   * インデックスに負の値を指定した場合は、指定された文字数分次に進める。
   * <br>
   * インデックスの取り得る範囲は-1〜n（nはコード・ポイント単位の文字列長）で
   * あり、前に戻すとこの範囲から外れる場合は、-1又はnにリミットをかける。
   * <br>
   * なお、ここでいうインデックスや文字数は、コード・ポイント単位の値をいう。
   *
   * @param  codePointCount 前に戻す文字数。
   * @return インデックスを移動した後のこのオブジェクト。
   */
  public StringSequence previous(int codePointCount)
  {
    if (codePointCount > 0) {
      _prev(codePointCount);
    }
    else if (codePointCount < 0){
      _next(-codePointCount);
    }

    return this;
  }

  /**
   * インデックスを、指定された文字数分だけ次に進める。
   * <br>
   * 引数には必ず正の値が指定されることを前提としている。
   * <br>
   * 次に進めるとインデックスの取り得る値の範囲（<tt>-1〜n</tt>、<tt>n</tt>
   * はコード・ポイント単位の文字列長）から外れる場合は、<tt>n</tt>を設定する。
   * <br>
   * なお、ここでいうインデックスや文字数は、コード・ポイント単位の値をいう。
   *
   * @param  codePointCount 次に進める文字数。
   */
  private void _next(int codePointCount)
  {
    assert (codePointCount > 0);

    int cpind = codePointIndex_ + codePointCount;
    try {
      charIndex_ = StringOperation.offsetByCodePoints(str_, 0, cpind);
      codePointIndex_ = cpind;
    }
    catch (Exception e) {
      charIndex_ = str_.length();
      codePointIndex_ = codePointCount_ ;
    }
  }

  /**
   * インデックスを、指定された文字数分だけ前に戻す。
   * <br>
   * 引数には必ず正の値が指定されることを前提としている。
   * <br>
   * 次に進めるとインデックスの取り得る値の範囲（<tt>-1〜n</tt>、<tt>n</tt>
   * はコード・ポイント単位の文字列長）から外れる場合は、<tt>-1</tt>を設定する。
   * <br>
   * なお、ここでいうインデックスや文字数は、コード・ポイント単位の値をいう。
   *
   * @param  codePointCount 次に進める文字数。
   */
  private void _prev(int codePointCount)
  {
    assert (codePointCount > 0);

    int cpind = codePointIndex_ - codePointCount;
    try {
      charIndex_ = StringOperation.offsetByCodePoints(str_, 0, cpind);
      codePointIndex_ = cpind;
    }
    catch (Exception e) {
      charIndex_ = -1;
      codePointIndex_ = -1;
    }
  }

  /**
   * 現在のインデックスを取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の値をいう。
   *
   * @return 現在のインデックス。
   */
  public int index()
  {
    return codePointIndex_ ;
  }

  /**
   * 現在のインデックスに位置する文字を取得する。
   * <br>
   * なお、ここでいう文字とは、コード・ポイントに対応する値をいう。
   *
   * @return 現在のインデックスに位置する文字。
   */
  public String character()
  {
    try {
      return str_.substring(charIndex_, 
        StringOperation.offsetByCodePoints(str_, charIndex_, 1));
    }
    catch (IndexOutOfBoundsException e) {
      return "";
    }
  }

  /**
   * 現在のインデックスから開始する部分文字列を取得する。
   *
   * @return 現在のインデックスから開始する部分文字列。
   */
  public String substring()
  {
    return (charIndex_ <= 0) ? str_ : str_.substring(charIndex_);
  }

  /**
   * 現在のインデックスから、指定された{@link ts.util.text.StringSequence 
   * StringSequence}の示すインデックスの前までの部分文字列を取得する。
   * <br>
   * 現在のインデックスに位置する文字は、部分文字列に含まれる。
   * また、引数のオブジェクトが示すインデックスに位置する文字は、部分文字列に
   * 含まれない。
   * <br>
   * このメソッドは、以下の操作と同様である:<code><pre>
   *     StringOperatin.substring(this.substring(), 0, endSeq.index())
   * </pre></code>
   *
   * @param  endSeq 取得する部分文字列の終了インデックスを保持する{@link
   *           ts.util.text.StringSequence StringSequence}オブジェクト。
   * @return このオブジェクトが参照する文字列の部分文字列。
   * @throws IndexOutOfBoundsException 引数の{@link ts.util.text.StringSequence
   *           StringSequence}がこのオブジェクトよりも前のインデックスを指して
   *           いる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public String substring(StringSequence endSeq)
    throws IndexOutOfBoundsException
  {
    assert (endSeq != null) : "@param:endSeq is null.";

    return this.str_.substring(
      Math.max(charIndex_,0), Math.max(endSeq.charIndex_, 0));
  }

  /**
   * 現在のインデックスの次から開始する部分文字列を取得する。
   *
   * @return 現在のインデックスの次から開始する部分文字列。
   */
  public String followingString()
  {
    if (charIndex_ < 0) {
      return str_ ;
    }
    else if (charIndex_ < str_.length()) {
      return str_.substring(
        StringOperation.offsetByCodePoints(str_, charIndex_, 1));
    }
    else {
      return "";
    }
  }

  /**
   * 現在のインデックスの次から、指定された{@link ts.util.text.StringSequence 
   * StringSequence}の示すインデックスの前までの部分文字列を取得する。
   * <br>
   * 現在のインデックスに位置する文字は、部分文字列に含まれない。
   * また、引数のオブジェクトが示すインデックスに位置する文字は、部分文字列に
   * 含まれない。
   * <br>
   * このメソッドは、以下の操作と同様である:<code><pre>
   *     StringOperatin.substring(this.substring(), 1, endSeq.index())
   * </pre></code>
   * 但し、現在のインデックスと引数のオブジェクトの示すインデックスが等しい場合
   * は空文字列を返す。
   *
   * @param  endSeq 取得する部分文字列の終了インデックスを保持する{@link
   *           ts.util.text.StringSequence StringSequence}オブジェクト。
   * @return このオブジェクトが参照する文字列の部分文字列。
   * @throws IndexOutOfBoundsException 引数の{@link ts.util.text.StringSequence
   *           StringSequence}がこのオブジェクトよりも前のインデックスを指して
   *           いる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public String followingString(StringSequence endSeq)
    throws IndexOutOfBoundsException
  {
    assert (endSeq != null) : "@param:endSeq is null.";

    int b = this.charIndex_ ;
    int e = endSeq.charIndex_ ;
    if (b == e) {
      return "";
    }

    int n = str_.length();

    b = (b < 0) ? 0 : ((b >= n) ?
      b : StringOperation.offsetByCodePoints(str_, b, 1));

    e = (e < 0) ? 0 : e;
    return str_.substring(b, e);
  }

  /**
   * 現在のインデックスから文末までの文字列の長さを取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の値をいう。
   *
   * @return 現在のインデックスから文末までの文字列の長さ。
   */
  public int restLength()
  {
    return codePointCount_ - codePointIndex_ ;
  }

  /**
   * 現在のインデックスから文末までの文字列中で、探索文字列に最初に一致する
   * インデックスを取得する。
   * <br>
   * 探索文字列が見つからなかった場合は、負の値を返す。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の先頭からの順番
   * （０から開始）である。
   * <br>
   * このメソッドは、以下の操作と同様である
   * （探索文字列が見つかる場合）:<code><pre>
   *     this.index() + StringOperation.indexOf(this.substring(), searched)
   * </pre></code>
   *
   * @param  searched 探索文字列。
   * @return 探索文字列に最初に一致するインデックス。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public int indexOf(String searched)
  {
    return StringOperation.indexOf(str_, searched, codePointIndex_);
  }

  /**
   * 現在のインデックスから文末までの文字列中で、探索文字列に最後に一致する
   * インデックスを取得する。
   * <br>
   * 探索文字列が見つからなかった場合は、負の値を返す。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイント単位の先頭からの順番
   * （０から開始）である。
   * <br>
   * このメソッドは、以下の操作と同様である
   * （探索文字列が見つかる場合）:<code><pre>
   *     this.index() + StringOperation.lastIndexOf(this.substring(), searched)
   * </pre></code>
   *
   * @param  searched 探索文字列。
   * @return 探索文字列に最後に一致するインデックス。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public int lastIndexOf(String searched)
  {
    int cpind = StringOperation.lastIndexOf(this.substring(), searched);
    return (cpind < 0) ? cpind : (Math.max(index(), 0) + cpind); 
  }

  /**
   * 現在のインデックスから文末までの部分文字列の先頭が、プリフィックスと一致
   * するかどうかを判定する。
   *
   * @param  prefix プリフィックス。
   * @return プリフィックスと一致する場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public boolean startsWith(String prefix)
  {
    return StringOperation.startsWith(this.substring(), prefix);
  }

  /**
   * 現在のインデックスから文末までの部分文字列の末尾が、サフィックスと一致する
   * かどうかを判定する。
   *
   * @param  suffix サフィックス。
   * @return サフィックスが一致する場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public boolean endsWith(String suffix)
  {
    return StringOperation.endsWith(this.substring(), suffix);
  }

  /**
   * 現在のインデックスから文末までの部分文字列の先頭が、指定された文字列集合
   * の要素のいずれかと一致するかどうかを判定する。
   * <br>
   * 一致した場合はその文字列を返し、一致する文字列が存在しなかった場合はヌル
   * を返す。
   *
   * @param  strs 文字列集合。
   * @return 一致した文字列。
   * @throws AssertionError 引数がヌルの場合、又は文字列集合の中にヌル又は
   *           空文字列が含まれている場合（デバッグ・モードのみ）。
   */
  public String startsWithOneOf(Collection<String> strs)
  {
    assert (strs != null) : "@param:strs is null.";
    assert (! strs.contains(null)) : "@param:strs contains null.";
    assert (! strs.contains("")) : "@param:strs contains an empty string.";

    for (String s : strs) {
      if (s != null && startsWith(s) && ! StringOperation.isEmpty(s)) {
        return s;
      }
    }

    return null;
  }

  /**
   * 現在のインデックスから文末までの部分文字列の先頭が、指定された文字列集合
   * の要素のいずれかと一致するかどうかを判定する。
   * <br>
   * 一致した場合はその文字列を返し、一致する文字列が存在しなかった場合はヌル
   * を返す。
   *
   * @param  seqs 文字列集合。
   * @return 一致した文字列。
   * @throws AssertionError 引数がヌルの場合、又は文字列集合の中にヌル又は
   *           空文字列が含まれている場合（デバッグ・モードのみ）。
   */
  public StringSequence startsWithOneOf(Collection<StringSequence> seqs)
  {
    assert (seqs != null) : "@param:seqs is null.";
    assert (! seqs.contains(null)) : "@param:seqs contains null.";
    assert (! seqs.contains(new StringSequence(""))) :
      "@param:seqs contains an empty string.";

    for (StringSequence s : seqs) {
      if (s != null && startsWith(s.substring()) && s.restLength() > 0) {
        return s;
      }
    }

    return null;
  }

  /**
   * 現在のインデックスから文末までの部分文字列に、探索文字列が含まれているか
   * どうかを判定する。
   *
   * @param  searched 探索文字列。
   * @return 探索文字列が含まれている場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public boolean contains(String searched)
  {
    return StringOperation.contains(this.substring(), searched);
  }

  /**
   * 指定された探索文字列が見つかるまで、インデックスを次に進める。
   * <br>
   * 探索文字列が見つからなかった場合は、インデックスの値がn（＝コード・
   * ポイント単位の文字列長）になる。
   *
   * @param  searched 探索文字列。
   * @return インデックスを移動した後のこのオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public StringSequence nextUntil(String searched)
  {
    assert (searched != null) : "@param:searched is null.";

    int findIndex = str_.indexOf(searched, charIndex_);
    if (findIndex < 0) {
      charIndex_ = str_.length();
      codePointIndex_ = codePointCount_;
    }
    else if (findIndex > 0) {
      charIndex_ = findIndex;
      codePointIndex_ = str_.codePointCount(0, findIndex);
    }
    else {
      charIndex_ = 0;
      codePointIndex_ = 0;
    }
    return this;
  }

  /**
   * 空白でない文字が現れるまで、インデックスを次に進める。
   * <br>
   * 移動後のインデックスは、スキップを開始してから最初に現れた空白でない文字
   * を指している。
   * <br>
   * 文字列の最後まで空白文字が続く場合は、インデックスの値がn（＝コード・
   * ポイント単位の文字列長）になる。
   *
   * @return インデックスを移動した後のこのオブジェクト。
   */
  public StringSequence skipWhitespaces()
  {
    for (int i=Math.max(charIndex_, 0);
        i<str_.length(); i+=str_.codePointCount(i, i+1)) {

      if (! Character.isWhitespace(str_.codePointAt(i))) {
        charIndex_ = i;
        codePointIndex_ = str_.codePointCount(0, i);
        return this;
      }
    }

    charIndex_ = str_.length();
    codePointIndex_ = codePointCount_ ;
    return this;
  }
}

