from whoosh.fields import *
from whoosh.index import create_in, open_dir
from whoosh.qparser import MultifieldParser
from whoosh.query import *
import abc
import copy
import csv
import json
import os.path
import sys

import utils


class GenericSearchEngine(object):
    """
    An abstract class for any search engine, whether that's an external API
    you've already built or a Whoosh-based search engine you can make from
    scratch via searchbetter.

    This class encapsulates some useful functionality like query rewriting
    that can benefit any search engine, even one not made using SearchBetter
    tools.

    Extending this class is easy - you just need to provide a search function
    and a few other details, and we'll build in functionality from there.
    """

    # make it an abstract class
    __metaclass__ = abc.ABCMeta

    def __init__(self):
        # no rewriter yet
        # TODO let someone pass this in the constructor
        self.rewriter = None

    def set_rewriter(self, rewriter):
        """
        Sets a new query rewriter (from this_package.rewriter) as the default
        rewriter for this search engine.
        """
        self.rewriter = rewriter


    def search(self, term):
        """
        Runs a plain-English search and returns results.
        :param term {String}: a query like you'd type into Google.
        :return: a list of dicts, each of which encodes a search result.
        """
        if self.rewriter is None:
          # if there's no query rewriter in place, just search for the
          # original term
          return self.single_search(term)
        else:
          # there's a rewriter! use it
          rewritten_queries = self.rewriter.rewrite(term)
          results = [self.single_search(q) for q in rewritten_queries]

          # results are multi-level... flatten it
          flattened_results = utils.flatten(results)

          return self.process_raw_results(flattened_results)


    def process_raw_results(self, raw_results):
        """
        After rewriting, we'll pass the full list of results in here
        for you to clean up. This could include sorting, removing duplicates,
        etc. (What you can do, and how you do it, really depends on what kind
        of objects your search engine returns.)
        """
        # default operation is a no-op
        return raw_results

    ###
    ###
    ### functions you need to specify
    ###
    ###

    def single_search(self, term):
        """
        Runs the search engine on a single term (no rewriting or anything),
        returning a list of objects.

        Subclasses must implement!

        :param str term: a word or phrase to search for
        :return: a list of objects that were found. Can be anything: dicts,
            strings, custom objects, whatever.
        :rtype: list(object)
        """
        raise NotImplementedError("Subclasses must implement!")



class WhooshSearchEngine(GenericSearchEngine):
  """
  An abstract class for custom, Whoosh-based search engines.

  A batteries-included search engine that can operate on any
  given dataset. Uses the Whoosh library to index and run searches
  on the dataset. Has built-in support for query rewriting.
  """

  # make it an abstract class
  __metaclass__ = abc.ABCMeta

  # TODO consider making more hierarchy. This is the WhooshSearchEngine,
  # which has the cool indexing capabilities. But more generally, you
  # could have a search engine that only has to support search().
  # but at that point it's just a useless interface, mostly.
  # anyway, such a search engine would let the query rewriting search engine
  # inherit from search engine too.

  def __init__(self, create, search_fields, index_path):
    """
    Creates a new search engine.

    :param create {bool}: If True, recreates an index from scratch.
        If False, loads the existing index
    :param search_fields {str[]}: An array names of fields in the index that our
        search engine will search against.
    :param index_path {str}: A relative path to a folder where the whoosh
        index should be stored.
    """

    super(WhooshSearchEngine, self).__init__()

    # TODO have an auto-detect feature that will determine if the
    # index exists, and depending on that creates or loads the index
    # TODO have the `create` option become `force_create`; normally
    #   it'll intelligently auto-generate, but if you force it it'll
    #   do what you say

    self.index_path = index_path

    # both these functions return an index
    if create:
      self.index = self.create_index()
    else:
      self.index = self.load_index()

    # set up searching
    # first, query parser
    self.parser = MultifieldParser(search_fields, self.index.schema)


  def load_index(self):
    """
    Used when the index is already created. This just loads it and
    returns it for you.
    """
    index = open_dir(self.index_path)
    return index


  def create_index(self):
    """
    Creates and returns a brand-new index. This will call
    get_empty_index() behind the scenes.
    Subclasses must implement!
    """
    raise NotImplementedError("Subclasses must implement!")


  def get_empty_index(self, path, schema):
    """
    Makes an empty index file, making the directory where it needs
    to be stored if necessary. Returns the index.

    This is called within create_index().
    TODO this breakdown is still confusing
    """
    if not os.path.exists(path):
      os.mkdir(path)
    index = create_in(path, schema)
    return index



  def get_num_documents(self):
    """
    Returns the number of documents in this search engine's corpus. That is,
    this is the size of the search engine.
    """
    query = Every()
    with self.index.searcher() as searcher:
      result = searcher.search(query)
      return len(result)

    return None


  def __len__(self):
    return self.get_num_documents()


  def single_search(self, term):
    """
    Helper function for search() that just returns search results for a
    single, non-rewritten search term.
    Returns a list of results, each of which is a Result object.
    The makeup of the results objects varies
    from search engine to search engine.

    OVERRIDDEN from GenericSearchEngine.
    """
    outer_results = []

    with self.index.searcher() as searcher:
      query_obj = self.parser.parse(term)
      # this variable is closed when the searcher is closed, so save this data
      # in a variable outside the with-block
      results = list(searcher.search(query_obj, limit=None))

      # this list of Hits, each of which has `fields()`` which is a dict version
      # of the item we got (contains title, description, or other fields)
      # `score` tells you how relevant the hit is (higher = better)
      cleaned_results = [WhooshResult(hit.fields(), hit.score) for hit in results]

      # make sure we store it outside the with-block b/c scope
      outer_results = cleaned_results

    return outer_results

  def process_raw_results(self, raw_results):
    # our search engine returns WhooshResult objects, so we can unique/sort
    # them
    # only give the unique ones
    # this works now that we use a Result object, which is hashable!
    unique_results = list(set(raw_results))

    # now let's sort all the results by their relevance score (descending
    # b/c higher is better)
    # so the best stuff bubbles to the top
    unique_results.sort(key=lambda result: result.score, reverse=True)

    return unique_results

class UdacitySearchEngine(WhooshSearchEngine):
  """
  Udacity
  """
  # DATASET_PATH = secure.DATASET_PATH_BASE+'udacity-api.json'
  # INDEX_PATH = secure.INDEX_PATH_BASE+'udacity'
  """Which dataset fields we should search over."""
  SEARCH_FIELDS = ["title", "subtitle", "expected_learning",
                   "syllabus", "summary", "short_summary"]

  def __init__(self, dataset_path, index_path, create=False):
    """
    Creates a new Udacity search engine.