package com.gmail.dpierron.calibre.trook;

import com.gmail.dpierron.calibre.configuration.ConfigurationManager;
import com.gmail.dpierron.calibre.configuration.DeviceMode;
import com.gmail.dpierron.calibre.datamodel.Author;
import com.gmail.dpierron.calibre.datamodel.Book;
import com.gmail.dpierron.calibre.datamodel.Series;
import com.gmail.dpierron.calibre.datamodel.Tag;
import com.gmail.dpierron.calibre.opds.JDOMManager;
import com.gmail.dpierron.tools.Helper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jdom2.Element;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.sql.*;
import java.util.*;

public class TrookSpecificSearchDatabaseManager {

  private static final int MIN_KEYWORD_LEN = 3;

  private static File databaseFile = null;
  private static Connection connection = null;
  private static Map<String, Long> keywords = new HashMap<String, Long>();
  private static List<Book> storedBooks = new LinkedList<Book>();
  private static List<Author> storedAuthors = new LinkedList<Author>();
  private static List<Series> storedSeries = new LinkedList<Series>();
  private static List<Tag> storedTags = new LinkedList<Tag>();
  private static long keywordCounter = 0;
  private static long resultCounter = 0;

  private static final Logger logger = LogManager.getLogger(TrookSpecificSearchDatabaseManager.class);

  public static void setDatabaseFile(File dbFile) {
    databaseFile = dbFile;
  }

  public static File getDatabaseFile() {
    return databaseFile;
  }

  public static Connection getConnection() {
    if (connection == null) {
      initConnection();
    }
    return connection;
  }

  public static void closeConnection() {
    if (connection != null) {
      try {
        connection.close();
      } catch (SQLException e) {
        // Ignore any exception
      }
    }
  }

  public static boolean databaseExists() {
    if (databaseFile == null)
      return false;
    if (!databaseFile.exists())
      return false;

    return true;
  }

  private static void initConnection() {
    try {
      Class.forName("org.sqlite.JDBC");
      if (databaseFile == null)
        return;
      if (databaseExists())
        databaseFile.delete();
      String url = databaseFile.toURI().getPath();
      connection = DriverManager.getConnection("jdbc:sqlite:" + url);
      Statement stat = connection.createStatement();
      // drop the indexes
      stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_RESULTS_RELATION_INDEX_ON_RESULTS;");
      stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_RESULTS_RELATION_INDEX_ON_KEYWORD;");
      stat.executeUpdate("DROP INDEX IF EXISTS RESULTS_INDEX;");
      stat.executeUpdate("DROP INDEX IF EXISTS KEYWORDS_INDEX;");
      // drop the tables
      stat.executeUpdate("DROP TABLE IF EXISTS keywords_results_relation");
      stat.executeUpdate("DROP TABLE IF EXISTS results");
      stat.executeUpdate("DROP TABLE IF EXISTS keywords");
      // create the tables
      stat.executeUpdate("CREATE TABLE keywords (keyword_id INTEGER PRIMARY KEY, keyword_value TEXT);");
      stat.executeUpdate("CREATE TABLE results (result_id INTEGER PRIMARY KEY, result_type TEXT, result_entry TEXT);");
      stat.executeUpdate("CREATE TABLE keywords_results_relation (keyword_id INTEGER, result_id INTEGER);");
      // create the indexes
      stat.executeUpdate("CREATE UNIQUE INDEX KEYWORDS_INDEX ON keywords(keyword_id ASC);");
      stat.executeUpdate("CREATE UNIQUE INDEX RESULTS_INDEX ON results(result_id ASC);");
      stat.executeUpdate("CREATE INDEX KEYWORDS_RESULTS_RELATION_INDEX_ON_KEYWORD ON keywords_results_relation(keyword_id ASC);");
      stat.executeUpdate("CREATE INDEX KEYWORDS_RESULTS_RELATION_INDEX_ON_RESULTS ON keywords_results_relation(result_id ASC);");
    } catch (ClassNotFoundException e) {
      logger.error(e); Helper.statsErrors++;
    } catch (SQLException e) {
      logger.error(e); Helper.statsErrors++;
    }
  }



  private static long addResult(String opdsEntry, ResultType type) throws SQLException {
    PreparedStatement ps = getConnection().prepareStatement("INSERT INTO results(result_id, result_type, result_entry) values (?, ?, ?);");
    ps.setLong(1, ++resultCounter);
    ps.setString(2, type.name().toUpperCase(Locale.ENGLISH));
    ps.setString(3, opdsEntry);
    ps.execute();
    return resultCounter;
  }

  private static long addKeyword(String keyword) throws SQLException {
    // check if keyword already exist (we test in memory to be quicker)
    if (keywords.containsKey(keyword))
      return keywords.get(keyword);
    else {
      PreparedStatement ps = getConnection().prepareStatement("INSERT INTO keywords(keyword_id, keyword_value) values (?, ?);");
      ps.setLong(1, ++keywordCounter);
      ps.setString(2, keyword);
      ps.execute();
      keywords.put(keyword, keywordCounter);
    }
    return keywordCounter;
  }

  private static void addKeywordResultRelation(long keywordId, long resultId) throws SQLException {
    PreparedStatement ps = getConnection().prepareStatement("INSERT INTO keywords_results_relation(keyword_id, result_id) values (?, ?);");
    ps.setLong(1, keywordId);
    ps.setLong(2, resultId);
    ps.execute();
  }

  private static List<String> keywordize(String pKeywordString) {
    List<String> keywords = new LinkedList<String>();
    List<String> result = new LinkedList<String>();
    String keywordString = pKeywordString.toUpperCase(Locale.ENGLISH);
    StringBuffer currentKeyword = null;
    for (int pos = 0; pos < keywordString.length(); pos++) {
      char c = keywordString.charAt(pos);
      if (!Character.isLetterOrDigit(c)) {
        // skip and break keywords at non letter or digits
        if (currentKeyword != null) {
          // process current keyword
          keywords.add(currentKeyword.toString());
          currentKeyword = null;
        }
        continue;
      }
      if (currentKeyword == null)
        currentKeyword = new StringBuffer();
      currentKeyword.append(c);
    }

    // process the last keyword
    if (currentKeyword != null) {
      keywords.add(currentKeyword.toString());
      currentKeyword = null;
    }

    // make packages with keywords, in increasing size
    for (int size = 1; size <= keywords.size(); size++) {
      int pos = 0;
      while (pos + size <= keywords.size()) {
        StringBuffer keywordPackage = new StringBuffer();
        for (int i = pos; i < pos + size; i++) {
          keywordPackage.append(keywords.get(i));
          keywordPackage.append(' ');
        }
        String s = keywordPackage.toString();
        result.add(s.substring(0, s.length() - 1));
        pos++;
      }
    }

    // add the "whole string" keyword
    result.add(keywordString);

    return result;
  }

  public static void addEntry(String opdsEntry, ResultType type, String... keywordStrings) throws SQLException {
    // add the result
    long resultId = addResult(opdsEntry, type);

    for (String keywordString : keywordStrings) {
      // process the keyword string
      List<String> keywords = keywordize(keywordString);
      for (String keyword : keywords) {
        if (keyword.length() >= MIN_KEYWORD_LEN) {
          long keywordId = addKeyword(keyword);
          addKeywordResultRelation(keywordId, resultId);
        }
      }
    }
  }

  private static void addEntryToTrookSearchDatabase(Element entry, ResultType type, String... keywords) {
    try {
      StringWriter sw = new StringWriter();
      JDOMManager.getCompactXML().output(entry, sw);
      String opdsEntry = sw.getBuffer().toString();
      addEntry(opdsEntry, type, keywords);
    } catch (IOException e) {
      logger.warn(e.getMessage()); Helper.statsWarnings++;
    } catch (SQLException e) {
      logger.warn(e.getMessage()); Helper.statsWarnings++;
    }
  }

  public static void addAuthor(Author item, Element entry) {
    if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) {
      if (!storedAuthors.contains(item)) {
        logger.debug("adding result for " + item);
        String keywords = item.getDisplayName();
        addEntryToTrookSearchDatabase(entry, ResultType.AUTHOR, keywords);
        storedAuthors.add(item);
      }
    }
  }

  public static void addSeries(Series item, Element entry) {
    if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) {
      if (!storedSeries.contains(item)) {
        logger.debug("adding result for " + item);
        String keywords = item.getDisplayName();
        // TODO do this after Doug has added support for noise words filtering in series 
        // String noNoise = item.getTitleForSort(ConfigurationManager.getCurrentProfile().getBookLanguageTag());
        addEntryToTrookSearchDatabase(entry, ResultType.SERIES, keywords);
        storedSeries.add(item);
      }
    }
  }

  public static void addTag(Tag item, Element entry) {
    if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) {
      if (!storedTags.contains(item)) {
        logger.debug("adding result for " + item);
        String keywords = item.getTextToSort();
        addEntryToTrookSearchDatabase(entry, ResultType.TAG, keywords);
        storedTags.add(item);
      }
    }
  }

  public static void addBook(Book item, Element entry) {
    if (ConfigurationManager.getCurrentProfile().getDeviceMode() == DeviceMode.Nook) {
      if (!storedBooks.contains(item)) {
        logger.debug("adding result for " + item);
        String keywords = item.getDisplayName();
        String noNoise = item.getSortName();
        addEntryToTrookSearchDatabase(entry, ResultType.BOOK, keywords, noNoise);
        storedBooks.add(item);
      }
    }
  }
}
