/*
 * StringConverter 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;
import java.util.List;
import java.util.ArrayList;

/**
 * 文字列を変換するメソッドを集めたクラス。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:52 $
 */
public class StringConverter
{
  /**
   * デフォルトコンストラクタ。
   */
  protected StringConverter()
  {}

  /**
   * ファイルから読み込んだ文字列などを通常の文字列に変換する。
   * <br>
   * 引数の文字列からエスケープ文字を除去し、トリムを行う。
   * 但し、エスケープ文字のすぐ後にある空白文字はトリムの対象としない。
   * <br>
   * 指定された特殊文字の前にエスケープ文字が存在するかどうかのチェックも
   * 行い、存在しなければ例外をスローする。
   * <br>
   * 特殊文字以外の文字の前にエスケープ文字が存在した場合はエスケープ文字を
   * 除去するだけとする。
   *
   * @param  readStr 読み込んだ文字列。
   * @param  escape エスケープ文字。
   * @param  specChars 特殊文字の集合。
   * @return 変換後の文字列。
   * @throws EndWithEscapeCharException エスケープ文字で終了した場合。
   * @throws StringConvertException 文字列の変換が完了しなかった場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   * @deprecated <code>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #fromReadString(String,String,Collection)}を使用して下さい。
   */
  @Deprecated
  public static String fromReadString(
    String readStr, char escape, CharSequence specChars
  ) throws EndWithEscapeCharException, StringConvertException
  {
    assert (readStr != null) : "@param:readStr is null.";
    assert (specChars != null) : "@param:specChars is null.";

    int len = readStr.length();
    int nSpec = specChars.length();

    StringBuilder buf = new StringBuilder(len);

    int i = 0;
    for ( ; i<len; i++) {
      char ch = readStr.charAt(i);
      if (!Character.isWhitespace(ch)) {
        break;
      }
    }

    int beginSpace = len;
    boolean isEsc = false;
    for ( ; i<len; i++) {
      char ch = readStr.charAt(i);
      if (isEsc) {
        buf.append(ch);
        beginSpace = buf.length();
        isEsc = false;
      }
      else if (ch == escape) {
        beginSpace = buf.length();
        isEsc = true;
      }
      else {
        for (int j=0; j<nSpec; j++) {
          if (ch == specChars.charAt(j)) {
            throw new StringConvertException(i, buf,
              "A special character is not escaped.");
          }
        }
        buf.append(ch);
        if (! Character.isWhitespace(ch)) {
          beginSpace = buf.length();
        }
      }
    }

    String convStr = buf.substring(0, beginSpace);
    if (isEsc) {
      throw new EndWithEscapeCharException(len - 1, convStr, null);
    }

    return convStr;
  }

  /**
   * ファイルから読み込んだ括弧で囲まれた文字列などを通常の文字列に変換する。
   * <br>
   * 引数の文字列から両端の括弧を除去し、エスケープ文字を除去する。
   * <br>
   * 両端に括弧がない場合は例外をスローする。
   * <br>
   * 指定された特殊文字の前にエスケープ文字が存在するかどうかのチェックも
   * 行い、存在しなければ例外をスローする。
   * なお、特殊文字には括弧も自動的に含まれるようにしている。
   * <br>
   * 特殊文字以外の文字の前にエスケープ文字が存在した場合はエスケープ文字を
   * 除去するだけとする。
   *
   * @param  readStr 読み込んだ文字列。
   * @param  escape エスケープ文字。
   * @param  openPar 開き括弧文字。
   * @param  closePar 閉じ括弧文字。
   * @param  specChars 特殊文字の集合。
   * @return 変換後の文字列。
   * @throws EndWithEscapeCharException エスケープ文字で終了した場合。
   * @throws StringConvertException 文字列の変換が完了しなかった場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   * @deprecated <code>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #fromReadString(String,String,String,String,Collection)}を使用して
   *   下さい。
   */
  @Deprecated
  public static String fromReadString(String readStr,
    char openPar, char closePar, char escape, CharSequence specChars
  ) throws EndWithEscapeCharException, StringConvertException
  {
    assert (readStr != null) : "@param:readStr is null.";
    assert (specChars != null) : "@param:specChars is null.";

    if (!readStr.startsWith(String.valueOf(openPar))) {
      throw new StringConvertException(0, "",
        "The specified string does not start with the parenthesis.");
    }

    int nSpec = specChars.length();

    StringBuilder buf = new StringBuilder();

    boolean isEsc = false;
    for (int i=1; i<readStr.length(); i++) {
      char ch = readStr.charAt(i);
      if (isEsc) {
        buf.append(ch);
        isEsc = false;
      }
      else if (ch == escape) {
        isEsc = true;
      }
      else if (ch == closePar) {
        if (i == readStr.length() - 1) {
          return buf.toString();
        }
        throw new StringConvertException(i, buf.toString(),
          "A special character is not escaped.");
      }
      else if (ch == openPar) {
        throw new StringConvertException(i, buf.toString(),
          "A special character is not escaped.");
      }
      else {
        for (int j=0; j<nSpec; j++) {
          if (ch == specChars.charAt(j)) {
            throw new StringConvertException(i, buf.toString(),
              "A special character is not escaped.");
          }
        }
        buf.append(ch);
      }
    }

    if (readStr.charAt(readStr.length() - 1) != closePar) {
      throw new EndWithEscapeCharException(readStr.length(), buf);
    }

    if (escape == closePar) {
      return buf.toString();
    }

    throw new EndWithEscapeCharException(readStr.length() - 1,
      buf.substring(0, buf.length() - 1), null);
  }

  /**
   * 通常の文字列をファイル出力用の文字列に変換する。
   * <br>
   * 引数の文字列に含まれる特殊文字の前にエスケープ文字を挿入する。
   * <br>
   * 引数の文字列が空白で開始している場合は、最初の空白文字の前にエスケープ文字
   * を挿入する。
   * 引数の文字列が空白で終了している場合は、最後の空白文字の前にエスケープ文字
   * を挿入する。
   *
   * @param  str 文字列。
   * @param  escape エスケープ文字。
   * @param  specChars 特殊文字の集合。
   * @return 変換後の文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   * @deprecated <code>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #toWriteString(String,String,Collection)}を使用して下さい。
   */
  @Deprecated
  public static String toWriteString(
    String str, char escape, CharSequence specChars)
  {
    assert (str != null) : "@param:str is null.";
    assert (specChars != null) : "@param:specChars is null.";

    int nSpec = specChars.length();
    int len = str.length();
    if (len == 0) {
      return "";
    }
    if (Character.isWhitespace(str.charAt(len-1))) {
      len --;
    }

    int i = 0;
    StringBuilder buf = new StringBuilder(len);
    if (Character.isWhitespace(str.charAt(0))) {
      buf.append(escape).append(str.charAt(0));
      i ++;
    }

    LOOP1:
    for (; i<len; i++) {
      char ch = str.charAt(i);
      if (ch == escape) {
        buf.append(escape).append(ch);
      }
      else {
        for (int j=0; j<nSpec; j++) {
          if (ch == specChars.charAt(j)) {
            buf.append(escape).append(ch);
            continue LOOP1;
          }
        }
        buf.append(ch);
      }
    }

    if (len > 0 && len == str.length() - 1) {
      buf.append(escape).append(str.charAt(len));
    }

    return buf.toString();
  }

  /**
   * 通常の文字列を括弧で囲まれたファイル出力用の文字列に変換する。
   * <br>
   * 引数の文字列の両端に括弧を付加する。
   * <br>
   * 引数の文字列に含まれる特殊文字の前にエスケープ文字を挿入する。
   * 引数の文字列に括弧が含まれる場合も、その前にエスケープ文字を挿入する。
   *
   * @param  str 文字列。
   * @param  openPar 開き括弧。
   * @param  closePar 閉じ括弧。
   * @param  escape エスケープ文字。
   * @param  specChars 特殊文字の集合。
   * @return 変換後の文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   * @deprecated <code>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #toWriteString(String,String,String,String,Collection)}を使用して
   *   下さい。
   */
  @Deprecated
  public static String toWriteString(String str, char openPar, char closePar,
    char escape, CharSequence specChars)
  {
    assert (str != null) : "@param:str is null.";
    assert (specChars != null) : "@param:specChars is null.";

    int nSpec = specChars.length();
    int len = str.length();

    StringBuilder buf = new StringBuilder(len + 2);
    buf.append(openPar);

    LOOP1:
    for (int i=0; i<len; i++) {
      char ch = str.charAt(i);
      if (ch == escape) {
        buf.append(escape).append(ch);
      }
      else if (ch == openPar) {
        buf.append(escape).append(ch);
      }
      else if (ch == closePar) {
        buf.append(escape).append(ch);
      }
      else {
        for (int j=0; j<nSpec; j++) {
          if (ch == specChars.charAt(j)) {
            buf.append(escape).append(ch);
            continue LOOP1;
          }
        }
        buf.append(ch);
      }
    }

    buf.append(closePar);
    return buf.toString();
  }

  /**
   * ファイルから読み込んだ文字列などを通常の文字列に変換する。
   * <br>
   * 引数の文字列からエスケープ文字列を除去し、トリムを行う。
   * 但し、エスケープ文字列のすぐ後にある空白文字はトリムの対象としない。
   * <br>
   * 指定された特殊文字の前にエスケープ文字列が存在するかどうかのチェックも
   * 行い、存在しなければ例外をスローする。
   * <br>
   * 特殊文字以外の文字の前にエスケープ文字列が存在した場合はエスケープ文字列を
   * 除去するだけとする。
   *
   * @param  readStr 読み込んだ文字列。
   * @param  escapeStr エスケープ文字列。
   * @param  specialStrs 特殊文字列の集合。
   * @return 変換後の文字列。
   *
   * @throws StringConvertException 文字列の変換が完了しなかった場合。
   * @throws IllegalArgumentException エスケープ文字列や特殊文字列に空文字列が
   *           含まれていた場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public static String fromReadString(
    String readStr, String escapeStr, Collection<String> specialStrs
  ) throws IllegalArgumentException, StringConvertException
  {
    assert (readStr != null) : "@param:readStr is null.";
    assert (escapeStr != null) : "@param:escapeStr is null.";
    assert (specialStrs != null) : "@param:specialStrs is null.";

    int escapeLen = StringOperation.length(escapeStr);
    if (escapeLen == 0) {
      throw new IllegalArgumentException("@param:escapeStr is empty.");
    }
    if (specialStrs.contains(null)) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains null.");
    }
    if (specialStrs.contains("")) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains an empty string.");
    }


    StringBuilder buf = new StringBuilder(readStr.length());
    StringSequence seq = new StringSequence(readStr).skipWhitespaces();
    String s;

    while (seq.validIndex()) {
      if (seq.startsWith(escapeStr)) {
        if (! seq.next(escapeLen).validIndex()) {
          throw new EndWithEscapeCharException(seq.index() - escapeLen, buf);
        }
        buf.append(seq.character());
        seq.next();
      }
      else if ((s = seq.startsWithOneOf(specialStrs)) != null) {
        throw new SpecialCharNotEscapedException(seq.index(), buf, s);
      }
      else if (StringOperation.isWhitespaces(seq.character())) {
        StringSequence bgn = seq.copy();
        if (! seq.skipWhitespaces().validIndex()) {
          break;
        }
        buf.append(bgn.substring(seq));
      }
      else {
        buf.append(seq.character());
        seq.next();
      }
    }

    return buf.toString();
  }

  /**
   * ファイルから読み込んだ括弧で囲まれた文字列などを通常の文字列に変換する。
   * <br>
   * 引数の文字列から両端の括弧を除去し、エスケープ文字列を除去する。
   * <br>
   * 両端に括弧がない場合は例外をスローする。
   * <br>
   * 指定された特殊文字や括弧の前にエスケープ文字列が存在するかどうかのチェック
   * も行い、存在しなければ例外をスローする。
   * <br>
   * 特殊文字や括弧以外の文字の前にエスケープ文字列が存在した場合はエスケープ
   * 文字列を除去するだけとする。
   *
   * @param  readStr 読み込んだ文字列。
   * @param  escapeStr エスケープ文字列。
   * @param  openPar 開き括弧文字列。
   * @param  closePar 閉じ括弧文字列。
   * @param  specialStrs 特殊文字列の集合。
   * @return 変換後の文字列。
   * @throws StringConvertException 文字列の変換が完了しなかった場合。
   * @throws IllegalArgumentException エスケープ文字列や括弧、特殊文字列に
   *           空文字列が含まれている場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public static String fromReadString(String readStr, String openPar,
    String closePar, String escapeStr, Collection<String> specialStrs
  ) throws IllegalArgumentException, StringConvertException
  {
    assert (readStr != null) : "@param:readStr is null.";
    assert (openPar != null) : "@param:openPar is null.";
    assert (closePar != null) : "@param:closePar is null.";
    assert (escapeStr != null) : "@param:escapeStr is null.";
    assert (specialStrs != null) : "@param:specialStrs is null.";

    int escapeLen = StringOperation.length(escapeStr);
    if (escapeLen == 0) {
      throw new IllegalArgumentException("@param:escapeStr is empty.");
    }
    int openParLen = StringOperation.length(openPar);
    if (openParLen == 0) {
      throw new IllegalArgumentException("@param:openPar is empty.");
    }
    int closeParLen = StringOperation.length(closePar);
    if (closeParLen == 0) {
      throw new IllegalArgumentException("@param:closePar is empty.");
    }
    if (specialStrs.contains(null)) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains null.");
    }
    if (specialStrs.contains("")) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains an empty string.");
    }

    if (! StringOperation.startsWith(readStr, openPar)) {
      throw new StartWithoutParenthesisException(0, "");
    }

    StringBuilder buf = new StringBuilder(readStr.length());
    StringSequence seq = new StringSequence(readStr).next(openParLen);
    String s;

    while (seq.validIndex()) {
      if (seq.startsWith(escapeStr)) {
        if (! seq.next(escapeLen).validIndex()) {
          if (escapeStr.equals(closePar)) {
            return buf.toString();
          }
          throw new EndWithEscapeCharException(seq.index() - escapeLen, buf);
        }
        buf.append(seq.character());
        seq.next();
      }
      else if (seq.startsWith(closePar)) {
        if (seq.next(closeParLen).validIndex()) {
          throw new SpecialCharNotEscapedException(
            seq.index() - closeParLen, buf, closePar);
        }
        return buf.toString();
      }
      else if (seq.startsWith(openPar)) {
        throw new SpecialCharNotEscapedException(seq.index(), buf, openPar);
      }
      else if ((s = seq.startsWithOneOf(specialStrs)) != null) {
        throw new SpecialCharNotEscapedException(seq.index(), buf, s);
      }
      else {
        buf.append(seq.character());
        seq.next();
      }
    }

    if (StringOperation.endsWith(readStr, closePar)) {
      throw new EndWithEscapeCharException(seq.index() - closeParLen,
        buf.substring(0, buf.length() - closeParLen));
    }

    throw new EndWithoutParenthesisException(seq.index(), buf);
  }

  /**
   * 通常の文字列をファイル出力用の文字列に変換する。
   * <br>
   * 引数の文字列に含まれる特殊文字の前にエスケープ文字列を挿入する。
   * <br>
   * 引数の文字列が空白で開始している場合は、最初の空白文字の前にエスケープ文字
   * 列を挿入する。
   * 引数の文字列が空白で終了している場合は、最後の空白文字の前にエスケープ文字
   * 列を挿入する。
   *
   * @param  str 文字列。
   * @param  escapeStr エスケープ文字列。
   * @param  specialStrs 特殊文字列の集合。
   * @return 変換後の文字列。
   * @throws IllegalArgumentException エスケープ文字列や特殊文字列が空文字列を
   *           含んでいる場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static String toWriteString(
    String str, String escapeStr, Collection<String> specialStrs)
  {
    assert (str != null) : "@param:str is null.";
    assert (escapeStr != null) : "@param:escapeStr is null.";
    assert (specialStrs != null) : "@param:specialStrs is null.";

    int escapeLen = StringOperation.length(escapeStr);
    if (escapeLen == 0) {
      throw new IllegalArgumentException("@param:escapeStr is empty.");
    }
    if (specialStrs.contains(null)) {
      throw new IllegalArgumentException("@param:specialStrs contains null.");
    }
    if (specialStrs.contains("")) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains an empty string.");
    }


    StringBuilder buf = new StringBuilder(str.length());
    StringSequence seq = new StringSequence(str);
    String s;

    if (StringOperation.isWhitespaces(seq.character())) {
      buf.append(escapeStr).append(seq.character());
      seq.next();
    }

    while (seq.validIndex()) {
      if (seq.startsWith(escapeStr)) {
        buf.append(escapeStr).append(escapeStr);
        seq.next(escapeLen);
      }
      else if ((s = seq.startsWithOneOf(specialStrs)) != null) {
        buf.append(escapeStr).append(s);
        seq.next(StringOperation.length(s));
      }
      else if(StringOperation.isWhitespaces(seq.character())&& !seq.hasNext()){
        buf.append(escapeStr).append(seq.character());
        break;
      }
      else {
        buf.append(seq.character());
        seq.next();
      }
    }

    return buf.toString();
  }

  /**
   * 通常の文字列を括弧で囲まれたファイル出力用の文字列に変換する。
   * <br>
   * 引数の文字列の両端に括弧を付加する。
   * <br>
   * 引数の文字列に含まれる特殊文字の前にエスケープ文字列を挿入する。
   * 引数の文字列に括弧が含まれる場合も、その前にエスケープ文字列を挿入する。
   *
   * @param  str 文字列。
   * @param  openPar 開き括弧。
   * @param  closePar 閉じ括弧。
   * @param  escapeStr エスケープ文字列。
   * @param  specialStrs 特殊文字列の集合。
   * @return 変換後の文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public static String toWriteString(String str,String openPar,String closePar,
    String escapeStr, Collection<String> specialStrs)
  {
    assert (str != null) : "@param:str is null.";
    assert (openPar != null) : "@param:openPar is null.";
    assert (closePar != null) : "@param:closePar is null.";
    assert (escapeStr != null) : "@param:escapeStr is null.";
    assert (specialStrs != null) : "@param:specialStrs is null.";

    int escapeLen = StringOperation.length(escapeStr);
    if (escapeLen == 0) {
      throw new IllegalArgumentException("@param:escapeStr is empty.");
    }
    int closeParLen = StringOperation.length(closePar);
    if (closeParLen == 0) {
      throw new IllegalArgumentException("@param:closePar is empty.");
    }
    int openParLen = StringOperation.length(openPar);
    if (openParLen == 0) {
      throw new IllegalArgumentException("@param:openPar is empty.");
    }
    if (specialStrs.contains(null)) {
      throw new IllegalArgumentException("@param:specialStrs contains null.");
    }
    if (specialStrs.contains("")) {
      throw new IllegalArgumentException(
        "@param:specialStrs contains an empty string.");
    }

    StringBuilder buf = new StringBuilder(str.length() + 2);
    buf.append(openPar);

    StringSequence seq = new StringSequence(str);
    String s;

    while (seq.validIndex()) {
      if (seq.startsWith(escapeStr)) {
        buf.append(escapeStr).append(escapeStr);
        seq.next(escapeLen);
      }
      else if (seq.startsWith(closePar)) {
        buf.append(escapeStr).append(closePar);
        seq.next(closeParLen);
      }
      else if (seq.startsWith(openPar)) {
        buf.append(escapeStr).append(openPar);
        seq.next(openParLen);
      }
      else if ((s = seq.startsWithOneOf(specialStrs)) != null) {
        buf.append(escapeStr).append(s);
        seq.next(StringOperation.length(s));
      }
      else {
        buf.append(seq.character());
        seq.next();
      }
    }

    buf.append(closePar);
    return buf.toString();
  }
}
