/**
 * This is free and unencumbered software released into the public domain.
 * 
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 * 
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * 
 * For more information, please refer to <http://unlicense.org/>
 */

package com.github.pffy.chinese.query;

import com.github.pffy.chinese.PinyinScore;


/**
 * MysqlPinyinQuery.java - Builds query from input.
 * 
 * @author The Pffy Authors
 * @version 0.8
 * 
 */

public final class MysqlPinyinQuery {

  // output
  private final String sql;

  // constants (can be properties)
  private final String TABLE = "pffydict";
  private final int MAX_LIMIT = 100;
  private final String DEFAULT_SQL_QUERY =
      "SELECT 'No query found.' as pffy FROM " + this.TABLE + " LIMIT 1";

  // ORDERBY terms
  private final String ORDERBY_SCORE_0_ONE = " priority DESC, d, slen, dlen ";
  private final String ORDERBY_SCORE_0_MANY =
      "  priority DESC, pmatch DESC, dmatch DESC, dlen ";
  private final String ORDERBY_SCORE_49_MANY =
      " priority DESC, tmatch DESC, smatch DESC, dmatch DESC, slen, dlen ";
  private final String ORDERBY_SCORE_100_ONE = " priority DESC, slen ";
  private final String ORDERBY_SCORE_100_MANY = " priority DESC, slen, plen ";

  // input
  private final String query;

  private final String[] allTerms;
  private final QueryMode queryMode;

  private final int limit;
  private final int start;
  private final int count;
  private final int score;

  /**
   * Builds this object with default values.
   */
  public MysqlPinyinQuery() {
    this("", 0, 10);
  }

  /**
   * Builds this object with a query.
   * 
   * @param query string
   */
  public MysqlPinyinQuery(String query) {
    this(query, 0, 10);
  }

  /**
   * Builds this object with a query and a limit.
   * 
   * @param query string
   * @param limit number of results
   */
  public MysqlPinyinQuery(String query, int limit) {
    this(query, 0, limit);
  }

  /**
   * Builds
   * 
   * @param query string
   * @param start position in result set
   * @param limit number of results
   */
  public MysqlPinyinQuery(String query, int start, int limit) {

    String[] allTerms;
    int count, score;

    query = filterQuery(query);
    allTerms = query.split(" ");

    count = allTerms.length;

    try {
      score = Integer.valueOf("" + (new PinyinScore(query)));
    } catch (NumberFormatException nfe) {
      score = 0;
    }

    this.queryMode = setQueryMode(count, score);

    this.query = query;
    this.allTerms = allTerms;
    this.start = Math.min(Math.abs(start), this.MAX_LIMIT);
    this.limit = Math.min(Math.abs(limit), this.MAX_LIMIT);

    this.score = score;
    this.count = count;

    this.sql = buildSql();
  }

  /**
   * Returns string representation of this object.
   * @return SQL string
   */
  @Override
  public final String toString() {
    return this.sql;
  }

  /**
   * Returns query input for this object.
   * @return query string
   */
  public final String getQueryString() {
    return this.query;
  }

  /**
   * Retruns SQL query string for this object.
   * @return SQL string
   */
  public final String getSql() {
    return this.sql;
  }

  /**
   * Returns number of terms in the input query.
   * 
   * @return count
   */
  public int getCount() {
    return this.count;
  }

  /**
   * Returns pinyin score of the input query, from 0 to 100
   * 
   * @return score
   */
  public int getPinyinScore() {
    return this.score;
  }

  /**
   * Returns the mode of the query based on number terms and pinyin score.
   * 
   * @return query mode
   */
  public QueryMode getQueryMode() {
    return this.queryMode;
  }

  // builds sql query
  private String buildSql() {

    String query, table, firstTerm;
    String[] allTerms;

    int start, limit;

    query = this.query;

    if (query.isEmpty()) {
      return this.DEFAULT_SQL_QUERY;
    }

    start = this.start;
    limit = this.limit;

    table = this.TABLE;

    allTerms = this.allTerms;
    firstTerm = allTerms[0];

    return " SELECT "

    // basics
        + " t, s, p, d, "

        // tmatch
        + " STRCMP('%" + firstTerm + "%', t) as tmatch, "
    // smatch
        + " STRCMP('%" + firstTerm + "%', s) as smatch, "
        // pmatch
        + " STRCMP('%" + firstTerm + "%', p) as pmatch, "
        // dmatch
        + " STRCMP('%" + firstTerm + "%', d) as dmatch, "

        // pbashmatch
        + " STRCMP('%" + firstTerm + "%', pbash) as pbashmatch, "


        // tlen
        + " LENGTH(t) as tlen, "
        // slen
        + " LENGTH(s) as slen, "
        // plen
        + " LENGTH(p) as plen, "
        // dlen
        + " LENGTH(d) as dlen, "


        // priority 1 to 9
        + " priority "

        // subquery starts
        + " FROM ( "

        // Priority 9 sub query
        + " SELECT t, s, p, d, tlen, pbash, '9' as priority "
        //
        + " FROM " + table
        //
        + " WHERE " + buildWhereTerms(allTerms, 9) + " "

        // right join
        + " UNION "
        // Priority 7 sub query
        + " SELECT t, s, p, d, tlen, pbash, '7' as priority "
        //
        + " FROM " + table
        //
        + " WHERE " + buildWhereTerms(allTerms, 7) + " "

        // right join
        + " UNION "
        // Priority 5 sub query
        + " SELECT t, s, p, d, tlen, pbash, '5' as priority "
        //
        + " FROM " + table
        //
        + " WHERE " + buildWhereTerms(allTerms, 5) + " "

        // right join
        + " UNION "
        // Priority 3 sub query
        + " SELECT t, s, p, d, tlen, pbash, '3' as priority "
        //
        + " FROM " + table
        //
        + " WHERE " + buildWhereTerms(allTerms, 3) + " "

        // dummy alias
        + " ) AS meh "

        // exactly one GROUP BY clause in the query
        + " GROUP BY s "
        // exactly one ORDER BY clause in the query
        + " ORDER BY " + buildOrderTerms()
        // exactly one LIMIT clause in the SQL subquery
        + " LIMIT " + start + " , " + limit;
  }

  // sets query mode
  private QueryMode setQueryMode(int score, int count) {

    QueryMode queryMode = QueryMode.SCORE_100_ONE;

    if (score == 100) {
      queryMode =
          (count > 1) ? QueryMode.SCORE_100_MANY : QueryMode.SCORE_100_ONE;
    } else if (score > 49) {
      queryMode = QueryMode.SCORE_49_MANY;
    } else {
      queryMode = (count > 1) ? QueryMode.SCORE_0_MANY : QueryMode.SCORE_0_ONE;
    }

    return queryMode;
  }

  // build ORDER BY clause terms
  private String buildOrderTerms() {

    switch (QueryMode.valueOf(this.queryMode.name())) {
      case SCORE_100_MANY:
        return this.ORDERBY_SCORE_100_MANY;
      case SCORE_49_MANY:
        return this.ORDERBY_SCORE_49_MANY;
      case SCORE_0_ONE:
        return this.ORDERBY_SCORE_0_ONE;
      case SCORE_0_MANY:
        return this.ORDERBY_SCORE_0_MANY;
      default:
        return this.ORDERBY_SCORE_100_ONE;
    }
  }

  // builds WHERE clause terms
  private String buildWhereTerms(String[] terms, int priority) {

    String str = "";

    String preTerm = "%";
    String postTerm = "%";

    switch (priority) {

      case 9:

        preTerm = "";
        postTerm = "/%";

        str = ""

        // open term
            + " ( "

            // t
            + "t LIKE '" + preTerm + terms[0] + postTerm + "' "
        // s
            + "OR s LIKE '" + preTerm + terms[0] + postTerm + "' "
            // pbash
            + "OR pbash LIKE '" + preTerm + terms[0] + postTerm + "' "
            // pmash
            + "OR pmash LIKE '" + preTerm + terms[0] + postTerm + "' "
            // d
            + "OR d LIKE '" + preTerm + terms[0] + postTerm + "' "

            // close term
            + " ) ";

        return str;
      case 7:

        // similar
        for (int i = 0; i < terms.length; i++) {


          preTerm = "%";
          postTerm = "%";

          if (i > 0) {
            str += " AND ";
          }

          if (i < 1) {
            preTerm = "";
          }

          if ((i + 1) == terms.length) {
            postTerm = "";
          }

          str += ""

          // open term
              + " ( "

              // t
              + "t LIKE '" + preTerm + terms[i] + postTerm + "' "
          // s
              + "OR s LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pbash
              + "OR pbash LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pmash
              + "OR pmash LIKE '" + preTerm + terms[i] + postTerm + "' "
              // d
              + "OR d LIKE '" + preTerm + terms[i] + postTerm + "' "

              // close term
              + " ) ";

          // repeat for each term
        }

        return str;
      case 5:

        // (A OR B) AND (A OR B)
        for (int i = 0; i < terms.length; i++) {


          if (i > 0) {
            str += " AND ";
          }

          str += ""

          // open term
              + " ( "

              // t
              + "t LIKE '" + preTerm + terms[i] + postTerm + "' "
          // s
              + "OR s LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pbash
              + "OR pbash LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pmash
              + "OR pmash LIKE '" + preTerm + terms[i] + postTerm + "' "
              // d
              + "OR d LIKE '" + preTerm + terms[i] + postTerm + "' "

              // close term
              + " ) ";

          // repeat for each term
        }

        return str;
      case 3:
        // (A OR B) OR (A OR B)
        for (int i = 0; i < terms.length; i++) {

          if (i > 0) {
            str += " OR ";
          }

          str += ""

          // open term
              + " ( "

              // t
              + "t LIKE '" + preTerm + terms[i] + postTerm + "' "
          // s
              + "OR s LIKE '" + preTerm + terms[i] + postTerm + "' "
              // d
              + "OR d LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pbash
              + "OR pbash LIKE '" + preTerm + terms[i] + postTerm + "' "
              // pmash
              + "OR pmash LIKE '" + preTerm + terms[i] + postTerm + "' "

              // close term
              + " ) ";

          // repeat for each term
        }

        // no break here, for now.
      default:
        // default is case 3, for now.
        return str;
    }

  }

  // filters query input
  private String filterQuery(String str) {
    str = removePunctutation(str);
    str = atomize(str);
    return str;
  }

  // removes punctuation
  private String removePunctutation(String str) {
    return str.replaceAll("(?![@',&])\\p{Punct}", " ");
  }

  // retains exactly one space between pinyin units
  private String atomize(String str) {
    return vacuum(str.replaceAll("\\w{1,6}[1-5]{1}", " {$1} "));
  }

  // reduces to exactly one space between units
  private String vacuum(String str) {
    return str.replaceAll("\\s{2,}", " ").trim();
  }

}
