# encoding: utf-8

from __future__ import absolute_import, division, print_function, unicode_literals

import json
import os
import re
import shutil
import threading
import warnings

import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
from haystack.inputs import Clean, Exact, PythonData, Raw
from haystack.models import SearchResult
from haystack.utils import get_identifier, get_model_ct
from haystack.utils import log as logging
from haystack.utils.app_loading import haystack_get_model
from jieba.analyse import ChineseAnalyzer
from whoosh import index
from whoosh.analysis import StemmingAnalyzer
from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
from whoosh.fields import ID as WHOOSH_ID
from whoosh.filedb.filestore import FileStorage, RamStorage
from whoosh.highlight import ContextFragmenter, HtmlFormatter
from whoosh.highlight import highlight as whoosh_highlight
from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter

try:
    import whoosh
except ImportError:
    raise MissingDependency(
        "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")

if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
    raise MissingDependency(
        "The 'whoosh' backend requires version 2.5.0 or greater.")

"""日期时间格式正则（用于解析Whoosh返回的日期字符串）"""
DATETIME_REGEX = re.compile(
    '^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
"""线程本地存储（用于RAM缓存）"""
LOCALS = threading.local()
LOCALS.RAM_STORE = None


class WhooshHtmlFormatter(HtmlFormatter):
    """
    自定义HTML高亮格式化器：简化Whoosh默认格式化器
    确保与其他搜索后端（Solr、Elasticsearch）的高亮结果格式一致
    """
    template = '<%(tag)s>%(t)s</%(tag)s>'


class WhooshSearchBackend(BaseSearchBackend):

    RESERVED_WORDS = (
        'AND',
        'NOT',
        'OR',
        'TO',
    )

    RESERVED_CHARACTERS = (
        '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
        '[', ']', '^', '"', '~', '*', '?', ':', '.',
    )

    def __init__(self, connection_alias, **connection_options):
        super(
            WhooshSearchBackend,
            self).__init__(
            connection_alias,
            **connection_options)
        self.setup_complete = False
        self.use_file_storage = True
        self.post_limit = getattr(
            connection_options,
            'POST_LIMIT',
            128 * 1024 * 1024)
        self.path = connection_options.get('PATH')

        """若配置为非文件存储，则使用RAM存储"""
        if connection_options.get('STORAGE', 'file') != 'file':
            self.use_file_storage = False
        """文件存储模式下必须指定路径"""
        if self.use_file_storage and not self.path:
            raise ImproperlyConfigured(
                "You must specify a 'PATH' in your settings for connection '%s'." %
                connection_alias)

        self.log = logging.getLogger('haystack')

    def setup(self):
        """
        延迟初始化：在首次使用时创建索引存储和Schema
        避免项目启动时过早加载资源
        """
        from haystack import connections
        new_index = False

        if self.use_file_storage and not os.path.exists(self.path):
            os.makedirs(self.path)
            new_index = True

        if self.use_file_storage and not os.access(self.path, os.W_OK):
            raise IOError(
                "The path to your Whoosh index '%s' is not writable for the current user/group." %
                self.path)

        if self.use_file_storage:
            self.storage = FileStorage(self.path)
        else:
            global LOCALS

            if getattr(LOCALS, 'RAM_STORE', None) is None:
                LOCALS.RAM_STORE = RamStorage()

            self.storage = LOCALS.RAM_STORE

        self.content_field_name, self.schema = self.build_schema(
            connections[self.connection_alias].get_unified_index().all_searchfields())
        self.parser = QueryParser(self.content_field_name, schema=self.schema)

        if new_index is True:
            self.index = self.storage.create_index(self.schema)
        else:
            try:
                self.index = self.storage.open_index(schema=self.schema)
            except index.EmptyIndexError:
                self.index = self.storage.create_index(self.schema)

        self.setup_complete = True

    def _create_field(self, field_class):
        """根据字段类型创建对应的Whoosh字段"""
        if field_class.is_multivalued:
            if field_class.indexed is False:
                return IDLIST(stored=True, field_boost=field_class.boost)
            else:
                return KEYWORD(stored=True, commas=True, scorable=True, field_boost=field_class.boost)
        elif field_class.field_type in ['date', 'datetime']:
            return DATETIME(stored=field_class.stored, sortable=True)
        elif field_class.field_type == 'integer':
            return NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost)
        elif field_class.field_type == 'float':
            return NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost)
        elif field_class.field_type == 'boolean':
            return BOOLEAN(stored=field_class.stored)
        elif field_class.field_type == 'ngram':
            return NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
        elif field_class.field_type == 'edge_ngram':
            return NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored,
                              field_boost=field_class.boost)
        else:
            return TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)

    def build_schema(self, fields):
        """初始化固定字段"""
        schema_fields = {
            ID: WHOOSH_ID(stored=True, unique=True),
            DJANGO_CT: WHOOSH_ID(stored=True),
            DJANGO_ID: WHOOSH_ID(stored=True),
        }
        initial_key_count = len(schema_fields)
        content_field_name = ''

        """遍历并创建动态字段"""
        for field_name, field_class in fields.items():
            field = _create_field(field_class)
            schema_fields[field_class.index_fieldname] = field
            if field_class.document is True:
                content_field_name = field_class.index_fieldname
                field.spelling = True

        """校验字段数量"""
        if len(schema_fields) <= initial_key_count:
            raise SearchBackendError(
                "No fields were found in any search_indexes. Please correct this before attempting to search."
            )

        return (content_field_name, Schema(**schema_fields))

    def _process_doc(self, doc):
        """处理文档字段的编码和boost字段清理"""
        for key in doc:
            doc[key] = self._from_python(doc[key])
        if 'boost' in doc:
            del doc['boost']
        return doc

    def _handle_update_error(self, e, obj, index):
        """处理更新文档时的异常"""
        if not self.silently_fail:
            raise
        self.log.error(
            u"%s while preparing object for update" % e.__class__.__name__,
            exc_info=True,
            extra={
                "data": {
                    "index": index,
                    "object": get_identifier(obj)}})

    def update(self, index, iterable, commit=True):
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()
        writer = AsyncWriter(self.index)

        for obj in iterable:
            try:
                doc = index.full_prepare(obj)
            except SkipDocument:
                self.log.debug(u"Indexing for object `%s` skipped", obj)
                continue  # 跳过当前对象，处理下一个
            # 处理文档格式
            processed_doc = self._process_doc(doc)
            # 尝试更新文档
            try:
                writer.update_document(**processed_doc)
            except Exception as e:
                self._handle_update_error(e, obj, index)

        if len(iterable) > 0:
            writer.commit()

    def remove(self, obj_or_string, commit=True):
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()
        whoosh_id = get_identifier(obj_or_string)

        try:
            self.index.delete_by_query(
                q=self.parser.parse(
                    u'%s:"%s"' %
                    (ID, whoosh_id)))
        except Exception as e:
            if not self.silently_fail:
                raise

            self.log.error(
                "Failed to remove document '%s' from Whoosh: %s",
                whoosh_id,
                e,
                exc_info=True)

    def clear(self, models=None, commit=True):
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()

        if models is not None:
            assert isinstance(models, (list, tuple))

        try:
            if models is None:
                self.delete_index()
            else:
                models_to_delete = []

                for model in models:
                    models_to_delete.append(
                        u"%s:%s" %
                        (DJANGO_CT, get_model_ct(model)))

                self.index.delete_by_query(
                    q=self.parser.parse(
                        u" OR ".join(models_to_delete)))
        except Exception as e:
            if not self.silently_fail:
                raise

            if models is not None:
                self.log.error(
                    "Failed to clear Whoosh index of models '%s': %s",
                    ','.join(models_to_delete),
                    e,
                    exc_info=True)
            else:
                self.log.error(
                    "Failed to clear Whoosh index: %s", e, exc_info=True)

    def delete_index(self):
        if self.use_file_storage and os.path.exists(self.path):
            shutil.rmtree(self.path)
        elif not self.use_file_storage:
            self.storage.clean()

        self.setup()

    def optimize(self):
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()
        self.index.optimize()

    def calculate_page(self, start_offset=0, end_offset=None):
        if end_offset is not None and end_offset <= 0:
            end_offset = 1

        page_num = 0

        if end_offset is None:
            end_offset = 1000000

        if start_offset is None:
            start_offset = 0

        page_length = end_offset - start_offset

        if page_length and page_length > 0:
            page_num = int(start_offset / page_length)

        page_num += 1
        return page_num, page_length

    @log_query
    def _handle_sorting(self, sort_by):
        """处理排序逻辑，返回排序字段和是否逆序"""
        if sort_by is None:
            return None, False

        reverse_counter = sum(1 for order_by in sort_by if order_by.startswith('-'))
        if reverse_counter and reverse_counter != len(sort_by):
            raise SearchBackendError("Whoosh requires all order_by fields to use the same sort direction")

        sort_by_list = [order_by[1:] if order_by.startswith('-') else order_by for order_by in sort_by]
        reverse = sort_by[0].startswith('-')
        return sort_by_list[0], reverse

    def _handle_model_filters(self, models, limit_to_registered_models):
        """处理模型过滤逻辑，返回窄查询集合"""
        if limit_to_registered_models is None:
            limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)

        model_choices = []
        if models and len(models):
            model_choices = sorted(get_model_ct(model) for model in models)
        elif limit_to_registered_models:
            model_choices = self.build_models_list()

        narrow_queries = set()
        if len(model_choices) > 0:
            narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
        return narrow_queries

    def _process_narrow_queries(self, narrow_queries):
        """处理窄查询，返回过滤后的结果集"""
        if not narrow_queries:
            return None

        narrow_searcher = self.index.searcher()
        narrowed_results = None
        for nq in narrow_queries:
            recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_str(nq)), limit=None)
            if len(recent_narrowed_results) <= 0:
                narrow_searcher.close()
                return {
                    'results': [],
                    'hits': 0,
                }
            if narrowed_results:
                narrowed_results.filter(recent_narrowed_results)
            else:
                narrowed_results = recent_narrowed_results
        return narrowed_results, narrow_searcher

    def _execute_search(self, parsed_query, page_num, page_length, sort_by, reverse, narrowed_results):
        """执行搜索并处理原始结果"""
        searcher = self.index.searcher()
        search_kwargs = {
            'pagelen': page_length,
            'sortedby': sort_by,
            'reverse': reverse,
        }
        if narrowed_results is not None:
            search_kwargs['filter'] = narrowed_results

        try:
            raw_page = searcher.search_page(parsed_query, page_num, **search_kwargs)
        except ValueError:
            if not self.silently_fail:
                raise
            searcher.close()
            return {
                'results': [],
                'hits': 0,
                'spelling_suggestion': None,
            }

        if raw_page.pagenum < page_num:
            searcher.close()
            return {
                'results': [],
                'hits': 0,
                'spelling_suggestion': None,
            }

        results = self._process_results(
            raw_page,
            highlight=highlight,
            query_string=query_string,
            spelling_query=spelling_query,
            result_class=result_class)
        searcher.close()
        return results

    def _get_spelling_suggestion(self, query_string):
        """获取拼写建议"""
        if not self.include_spelling:
            return None
        return self.create_spelling_suggestion(query_string) if query_string else None

    def _return_empty_results(self, query_string):
        """返回空结果集及拼写建议"""
        spelling_suggestion = self._get_spelling_suggestion(query_string)
        return {
            'results': [],
            'hits': 0,
            'spelling_suggestion': spelling_suggestion,
        }

    def search(
            self,
            query_string,
            sort_by=None,
            start_offset=0,
            end_offset=None,
            fields='',
            highlight=False,
            facets=None,
            date_facets=None,
            query_facets=None,
            narrow_queries=None,
            spelling_query=None,
            within=None,
            dwithin=None,
            distance_point=None,
            models=None,
            limit_to_registered_models=None,
            result_class=None,
            **kwargs
    ):
        if not self.setup_complete:
            self.setup()

        """处理空查询和短查询"""
        if len(query_string) == 0 or (len(query_string) <= 1 and query_string != u'*'):
            return self._return_empty_results(query_string)
        query_string = force_str(query_string)

        """处理排序"""
        sort_field, reverse = self._handle_sorting(sort_by)

        """处理分面警告"""
        for facet_type in [facets, date_facets, query_facets]:
            if facet_type is not None:
                warnings.warn(f"Whoosh does not handle {facet_type.__class__.__name__} faceting.", Warning,
                              stacklevel=2)

        """处理模型过滤和窄查询"""
        model_narrow_queries = self._handle_model_filters(models, limit_to_registered_models)
        if narrow_queries is None:
            narrow_queries = set()
        narrow_queries.update(model_narrow_queries)
        narrowed_results, narrow_searcher = self._process_narrow_queries(narrow_queries)
        if isinstance(narrowed_results, dict):
            return narrowed_results

        """执行搜索前的空索引校验"""
        self.index = self.index.refresh()
        if not self.index.doc_count():
            return self._return_empty_results(query_string)

        """执行搜索"""
        searcher = self.index.searcher()
        parsed_query = self.parser.parse(query_string)
        if parsed_query is None:
            searcher.close()
            return self._return_empty_results(query_string)

        page_num, page_length = self.calculate_page(start_offset, end_offset)
        results = self._execute_search(parsed_query, page_num, page_length, sort_field, reverse, narrowed_results)

        """关闭窄查询搜索器"""
        if hasattr(narrow_searcher, 'close'):
            narrow_searcher.close()

        return results


    def more_like_this(
            self,
            model_instance,
            additional_query_string=None,
            start_offset=0,
            end_offset=None,
            models=None,
            limit_to_registered_models=None,
            result_class=None,
            **kwargs):
        if not self.setup_complete:
            self.setup()

        field_name = self.content_field_name
        narrow_queries = set()
        narrowed_results = None
        self.index = self.index.refresh()

        if limit_to_registered_models is None:
            limit_to_registered_models = getattr(
                settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)

        if models and len(models):
            model_choices = sorted(get_model_ct(model) for model in models)
        elif limit_to_registered_models:
            # Using narrow queries, limit the results to only models handled
            # with the current routers.
            model_choices = self.build_models_list()
        else:
            model_choices = []

        if len(model_choices) > 0:
            if narrow_queries is None:
                narrow_queries = set()

            narrow_queries.add(' OR '.join(
                ['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))

        if additional_query_string and additional_query_string != '*':
            narrow_queries.add(additional_query_string)

        narrow_searcher = None

        if narrow_queries is not None:
            narrow_searcher = self.index.searcher()

            for nq in narrow_queries:
                recent_narrowed_results = narrow_searcher.search(
                    self.parser.parse(force_str(nq)), limit=None)

                if len(recent_narrowed_results) <= 0:
                    return {
                        'results': [],
                        'hits': 0,
                    }

                if narrowed_results:
                    narrowed_results.filter(recent_narrowed_results)
                else:
                    narrowed_results = recent_narrowed_results

        page_num, page_length = self.calculate_page(start_offset, end_offset)

        self.index = self.index.refresh()
        raw_results = EmptyResults()

        if self.index.doc_count():
            query = "%s:%s" % (ID, get_identifier(model_instance))
            searcher = self.index.searcher()
            parsed_query = self.parser.parse(query)
            results = searcher.search(parsed_query)

            if len(results):
                raw_results = results[0].more_like_this(
                    field_name, top=end_offset)

            # Handle the case where the results have been narrowed.
            if narrowed_results is not None and hasattr(raw_results, 'filter'):
                raw_results.filter(narrowed_results)

        try:
            raw_page = ResultsPage(raw_results, page_num, page_length)
        except ValueError:
            if not self.silently_fail:
                raise

            return {
                'results': [],
                'hits': 0,
                'spelling_suggestion': None,
            }

        if raw_page.pagenum < page_num:
            return {
                'results': [],
                'hits': 0,
                'spelling_suggestion': None,
            }

        results = self._process_results(raw_page, result_class=result_class)
        searcher.close()

        if hasattr(narrow_searcher, 'close'):
            narrow_searcher.close()

        return results

    def _process_results(
            self,
            raw_page,
            highlight=False,
            query_string='',
            spelling_query=None,
            result_class=None):
        from haystack import connections
        results = []

        hits = len(raw_page)

        if result_class is None:
            result_class = SearchResult

        facets = {}
        spelling_suggestion = None
        unified_index = connections[self.connection_alias].get_unified_index()
        indexed_models = unified_index.get_indexed_models()

        for doc_offset, raw_result in enumerate(raw_page):
            score = raw_page.score(doc_offset) or 0
            app_label, model_name = raw_result[DJANGO_CT].split('.')
            additional_fields = {}
            model = haystack_get_model(app_label, model_name)

            if model and model in indexed_models:
                for key, value in raw_result.items():
                    index = unified_index.get_index(model)
                    string_key = str(key)

                    if string_key in index.fields and hasattr(
                            index.fields[string_key], 'convert'):
                        # Special-cased due to the nature of KEYWORD fields.
                        if index.fields[string_key].is_multivalued:
                            if value is None or len(value) == 0:
                                additional_fields[string_key] = []
                            else:
                                additional_fields[string_key] = value.split(
                                    ',')
                        else:
                            additional_fields[string_key] = index.fields[string_key].convert(
                                value)
                    else:
                        additional_fields[string_key] = self._to_python(value)

                del (additional_fields[DJANGO_CT])
                del (additional_fields[DJANGO_ID])

                if highlight:
                    sa = StemmingAnalyzer()
                    formatter = WhooshHtmlFormatter('em')
                    terms = [token.text for token in sa(query_string)]

                    whoosh_result = whoosh_highlight(
                        additional_fields.get(self.content_field_name),
                        terms,
                        sa,
                        ContextFragmenter(),
                        formatter
                    )
                    additional_fields['highlighted'] = {
                        self.content_field_name: [whoosh_result],
                    }

                result = result_class(
                    app_label,
                    model_name,
                    raw_result[DJANGO_ID],
                    score,
                    **additional_fields)
                results.append(result)
            else:
                hits -= 1

        if self.include_spelling:
            if spelling_query:
                spelling_suggestion = self.create_spelling_suggestion(
                    spelling_query)
            else:
                spelling_suggestion = self.create_spelling_suggestion(
                    query_string)

        return {
            'results': results,
            'hits': hits,
            'facets': facets,
            'spelling_suggestion': spelling_suggestion,
        }

    def create_spelling_suggestion(self, query_string):
        spelling_suggestion = None
        reader = self.index.reader()
        corrector = reader.corrector(self.content_field_name)
        cleaned_query = force_str(query_string)

        if not query_string:
            return spelling_suggestion

        # Clean the string.
        for rev_word in self.RESERVED_WORDS:
            cleaned_query = cleaned_query.replace(rev_word, '')

        for rev_char in self.RESERVED_CHARACTERS:
            cleaned_query = cleaned_query.replace(rev_char, '')

        # Break it down.
        query_words = cleaned_query.split()
        suggested_words = []

        for word in query_words:
            suggestions = corrector.suggest(word, limit=1)

            if len(suggestions) > 0:
                suggested_words.append(suggestions[0])

        spelling_suggestion = ' '.join(suggested_words)
        return spelling_suggestion

    def _from_python(self, value):
        if hasattr(value, 'strftime'):
            if not hasattr(value, 'hour'):
                value = datetime(value.year, value.month, value.day, 0, 0, 0)
        elif isinstance(value, bool):
            if value:
                value = 'true'
            else:
                value = 'false'
        elif isinstance(value, (list, tuple)):
            value = u','.join([force_str(v) for v in value])
        elif isinstance(value, (six.integer_types, float)):
            # Leave it alone.
            pass
        else:
            value = force_str(value)
        return value

    def _to_python(self, value):
        if value == 'true':
            return True
        elif value == 'false':
            return False

        if value and isinstance(value, six.string_types):
            possible_datetime = DATETIME_REGEX.search(value)

            if possible_datetime:
                date_values = possible_datetime.groupdict()

                for dk, dv in date_values.items():
                    date_values[dk] = int(dv)

                return datetime(
                    date_values['year'],
                    date_values['month'],
                    date_values['day'],
                    date_values['hour'],
                    date_values['minute'],
                    date_values['second'])

        try:
            converted_value = json.loads(value)

            if isinstance(
                    converted_value,
                    (list,
                     tuple,
                     set,
                     dict,
                     six.integer_types,
                     float,
                     complex)):
                return converted_value
        except (SyntaxError, ValueError):
            pass
        except BaseException as e:
            """对SystemExit、KeyboardInterrupt等系统级异常重新抛出"""
            if isinstance(e, (SystemExit, KeyboardInterrupt)):
                raise
        return value


class WhooshSearchQuery(BaseSearchQuery):
    def _convert_datetime(self, date):
        if hasattr(date, 'hour'):
            return force_str(date.strftime('%Y%m%d%H%M%S'))
        else:
            return force_str(date.strftime('%Y%m%d000000'))

    def clean(self, query_fragment):
        words = query_fragment.split()
        cleaned_words = []

        for word in words:
            if word in self.backend.RESERVED_WORDS:
                word = word.replace(word, word.lower())

            for char in self.backend.RESERVED_CHARACTERS:
                if char in word:
                    word = "'%s'" % word
                    break

            cleaned_words.append(word)

        return ' '.join(cleaned_words)

    def build_query_fragment(self, field, filter_type, value):
        from haystack import connections
        query_frag = ''
        is_datetime = False

        if not hasattr(value, 'input_type_name'):
            if hasattr(value, 'values_list'):
                value = list(value)

            if hasattr(value, 'strftime'):
                is_datetime = True

            if isinstance(value, six.string_types) and value != ' ':
                value = Clean(value)
            else:
                value = PythonData(value)

        prepared_value = value.prepare(self)

        if not isinstance(prepared_value, (set, list, tuple)):

            prepared_value = self.backend._from_python(prepared_value)

        if field == 'content':
            index_fieldname = ''
        else:
            index_fieldname = u'%s:' % connections[self._using].get_unified_index(
            ).get_index_fieldname(field)

        filter_types = {
            'content': '%s',
            'contains': '*%s*',
            'endswith': "*%s",
            'startswith': "%s*",
            'exact': '%s',
            'gt': "{%s to}",
            'gte': "[%s to]",
            'lt': "{to %s}",
            'lte': "[to %s]",
            'fuzzy': u'%s~',
        }

        if value.post_process is False:
            query_frag = prepared_value
        else:
            if filter_type in [
                'content',
                'contains',
                'startswith',
                'endswith',
                'fuzzy']:
                if value.input_type_name == 'exact':
                    query_frag = prepared_value
                else:

                    terms = []

                    if isinstance(prepared_value, six.string_types):
                        possible_values = prepared_value.split(' ')
                    else:
                        if is_datetime is True:
                            prepared_value = self._convert_datetime(
                                prepared_value)

                        possible_values = [prepared_value]

                    for possible_value in possible_values:
                        terms.append(
                            filter_types[filter_type] %
                            self.backend._from_python(possible_value))

                    if len(terms) == 1:
                        query_frag = terms[0]
                    else:
                        query_frag = u"(%s)" % " AND ".join(terms)
            elif filter_type == 'in':
                in_options = []

                for possible_value in prepared_value:
                    is_datetime = False

                    if hasattr(possible_value, 'strftime'):
                        is_datetime = True

                    pv = self.backend._from_python(possible_value)

                    if is_datetime is True:
                        pv = self._convert_datetime(pv)

                    if isinstance(pv, six.string_types) and not is_datetime:
                        in_options.append('"%s"' % pv)
                    else:
                        in_options.append('%s' % pv)

                query_frag = "(%s)" % " OR ".join(in_options)
            elif filter_type == 'range':
                start = self.backend._from_python(prepared_value[0])
                end = self.backend._from_python(prepared_value[1])

                if hasattr(prepared_value[0], 'strftime'):
                    start = self._convert_datetime(start)

                if hasattr(prepared_value[1], 'strftime'):
                    end = self._convert_datetime(end)

                query_frag = u"[%s to %s]" % (start, end)
            elif filter_type == 'exact':
                if value.input_type_name == 'exact':
                    query_frag = prepared_value
                else:
                    prepared_value = Exact(prepared_value).prepare(self)
                    query_frag = filter_types[filter_type] % prepared_value
            else:
                if is_datetime is True:
                    prepared_value = self._convert_datetime(prepared_value)

                query_frag = filter_types[filter_type] % prepared_value

        if len(query_frag) and not isinstance(value, Raw):
            if not query_frag.startswith('(') and not query_frag.endswith(')'):
                query_frag = "(%s)" % query_frag

        return u"%s%s" % (index_fieldname, query_frag)

class WhooshEngine(BaseEngine):
    backend = WhooshSearchBackend
    query = WhooshSearchQuery
