from datetime import datetime
from ldclient.enums import QueryConditionValueType

from .experimental_enums import QueryCompoundReturnType, QueryObservationConditionOperator, QueryConditionType, QueryEntityReturnType, QueryFrequency, QueryViewType
from ..base import BaseModel


class QueryCondition(BaseModel):
    """
    DEPRECATED. The underlying function that uses this model, update_lr_query_and_search_compounds, has been deprecated
    Use the more generic update_advanced_search_query function instead

    Represents a query condition object.

    'type': Enum with values ['structure', 'observation']
            Each value specifies type of query condition
    'id': Each condition has a unique numeric id assigned to it.
    'addable_column_id': Addable column id of the column to query on
    'value': value for the condition.
             Note: Certain wildcards that can be used while specifying the value -
                   "(defined)" is a wildcard matching all non-null values in query conditions.
                   "*" is a wildcard that matched any character zero or more times
    'value_type': Enum with values ['value', 'numeric_range', 'substructure']
                  'value' is used for simple query conditions not involving ranges of values.
                  'numeric_range' are used for range conditions
                  'substructure' is used for structure type conditions, to make search based on sketched substructure
    """

    _fields = ['type', 'id', 'addable_column_id', 'value', 'value_type']

    def __init__(self, type, id, value_type, addable_column_id=None, value=None):
        self.type = type
        self.id = id
        self.addable_column_id = addable_column_id
        self.value = value
        self.value_type = value_type


class QueryDataSource(BaseModel):
    """
    Represents a query data source object.

    :type inverted: :class:`bool`
    :param inverted: Whether or not search should invert results from the specified data source(s).

    :type value: :class:`str`
    :param value: Data source(s) to search; defaults to "(defined)", or all data sources.
    """
    _fields = ["inverted", "value"]

    def __init__(self, inverted=False, value="(defined)"):
        self.inverted = inverted
        self.value = value


class BaseQueryCondition(BaseModel):
    """
    Represents a generic query condition object.
    Not intended to be used directly -- instead, use one of
        - QueryObservationCondition
        - QueryRangedObservationCondition
        - QueryRelativeDateObservationConsition
        - QueryDateRangeObservationCondition
        - QueryStructureCondition
        - QueryLiveReportCondition
        - QueryProjectCondition
    """
    _fields = ["type", "id", "inverted", "value_type", "value", "delimiter"]
    delimiter = ""

    def __init__(self,
                 id=None,
                 inverted=False,
                 value=None,
                 value_type=None,
                 type=None,
                 delimiter=""):
        self.id = id
        self.inverted = inverted
        self.value = value
        self.value_type = value_type
        self.type = type
        self.delimiter = delimiter

    def serialize_value(self):
        """
        Serialize a query value.

        :rtype: :class:`str`
        :return: A delimited string that represents a query value
        """
        if self.value == "(defined)":
            return self.value
        elif self.value_type in (QueryConditionValueType.DATE_RANGE,
                                 QueryConditionValueType.NUMERIC_RANGE):
            if self.value[0] is None and self.value[1] is None:
                return "(defined)"
            else:
                lower = "" if self.value[0] is None else str(self.value[0])
                upper = "" if self.value[1] is None else str(self.value[1])
                return "[{},{}]".format(lower, upper)
        elif self.value_type == QueryConditionValueType.RELATIVE_DATE:
            return str(self.value)
        else:
            return self.delimiter.join(self.value)

    @classmethod
    def deserialize_value(cls, value):
        """
        Deserialize a query value.

        :type value: :class:`str`
        :param value: A delimited string that represents a query value

        :rtype: :class:`list` of :class:`str`
        :return: A list of values that constitutes a query value
        """
        if value == "(defined)":
            if cls.value_type in (QueryConditionValueType.DATE_RANGE,
                                  QueryConditionValueType.NUMERIC_RANGE):
                return (None, None)
            else:
                return value
        elif isinstance(value, str) and value.startswith("[") and value.endswith("]"):
            if cls.value_type == QueryConditionValueType.NUMERIC_RANGE:
                lower, upper = value.lstrip("[").rstrip("]").split(",")
                lower = None if lower == "" else float(lower)
                upper = None if upper == "" else float(upper)
                return (lower, upper)
            elif cls.value_type == QueryConditionValueType.DATE_RANGE:
                lower, upper = value.lstrip("[").rstrip("]").split(",")
                lower = None if lower == "" else datetime.strptime(lower, '%Y-%m-%d').date()
                upper = None if upper == "" else datetime.strptime(upper, '%Y-%m-%d').date()
                return (lower, upper)
        elif cls.value_type == QueryConditionValueType.RELATIVE_DATE:
            return str(value)
        else:
            return value.split(cls.delimiter) if cls.delimiter else value

    @classmethod
    def from_list(cls, arr):
        """
        Overrides BaseModel.from_list
        """

        def condition(t, vt):
            return type_lookup.get(t) or type_value_type_lookup.get((t, vt)) or cls

        type_lookup = {
            QueryConditionType.STRUCTURE: QueryStructureCondition,
            QueryConditionType.LIVE_REPORT: QueryLiveReportCondition,
            QueryConditionType.PROJECT: QueryProjectCondition
        }
        type_value_type_lookup = {
            (QueryConditionType.OBSERVATION, QueryConditionValueType.VALUE): QueryObservationCondition,
            (QueryConditionType.OBSERVATION, QueryConditionValueType.NUMERIC_RANGE): QueryRangedObservationCondition,
            (QueryConditionType.OBSERVATION, QueryConditionValueType.DATE_RANGE): QueryDateRangeObservationCondition,
            (QueryConditionType.OBSERVATION, QueryConditionValueType.RELATIVE_DATE): QueryRelativeDateObservationCondition
        }
        return [condition(elem.get("type"), elem.get("value_type")).from_dict(elem) for elem in arr]


class BaseQueryObservationCondition(BaseQueryCondition):
    """
    Represents an observation query condition object.
    Not intended to be used directly -- instead, use one of
        - QueryObservationCondition
        - QueryRangedObservationCondition
        - QueryRelativeDateObservationConsition
        - QueryDateRangeObservationCondition
    """
    _fields = BaseQueryCondition._fields + [
        "addable_column_id", "operator", "observation_date_inverted", "observation_date_value_type",
        "observation_date_value", "create_assay_limited_addable_column", "protocols", "units",
        "concentrations", "concentration_units", "related_column_conditions"
    ]
    delimiter = ""
    value_type = None

    def __init__(self,
                 addable_column_id,
                 value,
                 value_type=None,
                 delimiter="",
                 id=None,
                 inverted=False,
                 operator=QueryObservationConditionOperator.EQUALS,
                 observation_date_inverted=False,
                 observation_date_value_type=None,
                 observation_date_value=None,
                 create_assay_limited_addable_column=False,
                 protocols=None,
                 units=None,
                 concentrations=None,
                 concentration_units=None,
                 related_column_conditions=[],
                 **kwargs):
        super(BaseQueryObservationCondition, self).__init__(type=QueryConditionType.OBSERVATION,
                                                            id=id,
                                                            inverted=inverted,
                                                            value=value,
                                                            value_type=value_type,
                                                            delimiter=delimiter)
        self.addable_column_id = addable_column_id
        self.operator = operator
        self.observation_date_inverted = observation_date_inverted
        self.observation_date_value_type = observation_date_value_type
        self.observation_date_value = observation_date_value
        self.create_assay_limited_addable_column = create_assay_limited_addable_column
        self.protocols = protocols
        self.units = units
        self.concentrations = concentrations
        self.concentration_units = concentration_units
        self.related_column_conditions = related_column_conditions


class QueryRangedObservationCondition(BaseQueryObservationCondition):
    """
    Represents a numeric ranged query condition specific to an observation column.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier of the addable column referenced by the query condition.

    :type value: :class:`tuple` of :class:`float` or :class:`None`
    :param value: The desired numeric range.
        An unbounded part of the range is represented as `None`, e.g. (None, 0.1)

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition

    :type create_assay_limited_addable_column: :class:`bool`
    :param create_assay_limited_addable_column: Whether or not this query condition should create a Limited Assay Column.
    """
    delimiter = ""
    value_type = QueryConditionValueType.NUMERIC_RANGE

    def __init__(self,
                 addable_column_id,
                 value=(None, None),
                 id=None,
                 inverted=False,
                 create_assay_limited_addable_column=False,
                 **kwargs):
        super(QueryRangedObservationCondition, self).__init__(
            addable_column_id,
            value,
            value_type=QueryConditionValueType.NUMERIC_RANGE,
            id=id,
            inverted=inverted,
            create_assay_limited_addable_column=create_assay_limited_addable_column,
            delimiter=QueryRangedObservationCondition.delimiter)


class QueryObservationCondition(BaseQueryObservationCondition):
    """
    Represents a query condition specific to an observation column.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier of the addable column referenced by the query condition.

    :type value: :class:`list` of :class:`str`
    :param value: A list of a single value, or multiple values, to search for.
        There are two possible wildcards
         -"(defined)" to match all values
         -"*" to match globbed values.

        If using the "(defined)" wildcard, this should be the only value, i.e. `["(defined)"]`

        If using the "*" wildcard, this should be part of the value you would like to glob-search, e.g. `["CHEMBL*"]`

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition

    :type create_assay_limited_addable_column: :class:`bool`
    :param create_assay_limited_addable_column: Whether or not this query condition should create a Limited Assay Column.
    """
    delimiter = "__DEBUG__"
    value_type = QueryConditionValueType.VALUE

    def __init__(self,
                 addable_column_id,
                 value=["(defined)"],
                 id=None,
                 inverted=False,
                 create_assay_limited_addable_column=False,
                 **kwargs):
        super(QueryObservationCondition, self).__init__(
            addable_column_id,
            value,
            value_type=QueryConditionValueType.VALUE,
            id=id,
            inverted=inverted,
            delimiter=QueryObservationCondition.delimiter,
            create_assay_limited_addable_column=create_assay_limited_addable_column)


class QueryRelativeDateObservationCondition(BaseQueryObservationCondition):
    """
    Represents a relative date query condition specific to an observation column.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier of the date-type addable column referenced by the query condition.

    :type value: :class:`int`
    :param value: The number of days retrospective to today. Defaults to 7.
        Note that this value must be greater than 0.

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition

    :type create_assay_limited_addable_column: :class:`bool`
    :param create_assay_limited_addable_column: Whether or not this query condition should create a Limited Assay Column.
    """
    value_type = QueryConditionValueType.RELATIVE_DATE

    def __init__(self,
                 addable_column_id,
                 value=7,
                 id=None,
                 inverted=False,
                 create_assay_limited_addable_column=False,
                 **kwargs):
        super(QueryRelativeDateObservationCondition, self).__init__(
            addable_column_id,
            value,
            value_type=QueryConditionValueType.RELATIVE_DATE,
            id=id,
            inverted=inverted,
            create_assay_limited_addable_column=create_assay_limited_addable_column,
            delimiter=QueryRelativeDateObservationCondition.delimiter,
        )


class QueryDateRangeObservationCondition(BaseQueryObservationCondition):
    """
    Represents a date range query condition specific to an observation column.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier of the date-type addable column referenced by the query condition.

    :type value: :class:`tuple` of :class:`date:`
    :param value: The desired date range.
        An unbounded part of the range is represented as `None`, e.g. (None, date(1970, 1, 1))

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition

    :type create_assay_limited_addable_column: :class:`bool`
    :param create_assay_limited_addable_column: Whether or not this query condition should create a Limited Assay Column.
    """
    value_type = QueryConditionValueType.DATE_RANGE

    def __init__(self,
                 addable_column_id,
                 value=(None, None),
                 id=None,
                 inverted=False,
                 create_assay_limited_addable_column=False,
                 **kwargs):
        super(QueryDateRangeObservationCondition, self).__init__(
            addable_column_id,
            value,
            value_type=QueryConditionValueType.DATE_RANGE,
            id=id,
            inverted=inverted,
            create_assay_limited_addable_column=create_assay_limited_addable_column,
            delimiter=QueryDateRangeObservationCondition.delimiter,
        )


class QueryStructureCondition(BaseQueryCondition):
    """
    Represents a query condition specific to a structure.

    :type value: :class:`str`
    :param value: An MDL Molfile structure.

    :type search_type: :class:`~experimental_enums.QueryConditionValueType`
    :param search_type: The type of STRUCTURE search for this query condition.
        Must be one of:
            -QueryConditionValueType.EXACT
            -QueryConditionValueType.SUBSTRUCTURE
            -QueryConditionValueType.SIMILARITY

    :type scaffold_id: :class:`str`
    :param scaffold_id: The ID of the scaffold to use for structure search

    :type threshold: :class:`float`
    :param threshold: The percentage similarity of the substructure, from 0 to 1.
        Only to be used when search_type==QueryConditionValueType.SIMILARITY

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition
    """
    _fields = BaseQueryCondition._fields + ["scaffold_id", "threshold"]
    delimiter = ""
    value_type = None

    def __init__(self,
                 value,
                 value_type=QueryConditionValueType.SUBSTRUCTURE,
                 scaffold_id=None,
                 threshold=None,
                 id=None,
                 inverted=False,
                 **kwargs):
        super(QueryStructureCondition, self).__init__(type=QueryConditionType.STRUCTURE,
                                                      id=id,
                                                      inverted=inverted,
                                                      value=value,
                                                      value_type=value_type,
                                                      delimiter=QueryStructureCondition.delimiter)
        self.scaffold_id = scaffold_id
        self.threshold = threshold


class QueryLiveReportCondition(BaseQueryCondition):
    """
    Represents a query condition for the presence of entities in a LiveReport

    :type value: :class:`list` of :class:`str`
    :param value: The IDs of LiveReports to limit query results on

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition
    """
    _fields = BaseQueryCondition._fields
    delimiter = ","
    value_type = QueryConditionValueType.VALUE

    def __init__(self, value, id=None, inverted=False, **kwargs):
        super(QueryLiveReportCondition, self).__init__(type=QueryConditionType.LIVE_REPORT,
                                                       id=id,
                                                       inverted=inverted,
                                                       value=value,
                                                       value_type=QueryConditionValueType.VALUE,
                                                       delimiter=QueryLiveReportCondition.delimiter)


class QueryProjectCondition(BaseQueryCondition):
    """
    Represents a query condition for the presence of entities in a Project.

    :type value: :class:`list` of :class:`str`
    :param value: The IDs of Projects to limit query results on

    :type id: :class:`str`
    :param id: The ID of the query condition

    :type inverted: :class:`bool`
    :param inverted: Whether or not to invert results for this query condition
    """
    _fields = BaseQueryCondition._fields
    delimiter = ","
    value_type = QueryConditionValueType.VALUE

    def __init__(self, value, id=None, inverted=False, **kwargs):
        super(QueryProjectCondition, self).__init__(type=QueryConditionType.PROJECT,
                                                    id=id,
                                                    inverted=inverted,
                                                    value=value,
                                                    value_type=QueryConditionValueType.VALUE,
                                                    delimiter=QueryProjectCondition.delimiter)


class Query(BaseModel):
    """
    Represents an Advanced Search Query object.

    :type live_report_id: :class:`str`
    :param live_report_id: The LiveReport id of the Advanced Search Query.

    :type id: :class:`str`
    :param id: The id of the Advanced Search Query.

    :type active: :class:`bool`
    :param active: Whether or not the Advanced Search Query is enabled.

    :type auto_update_results: :class:`bool`
    :param auto_update_results: Whether or not results from the Advanced Search Query will auto-populate its LiveReport.

    :type clear_report_on_search: :class:`bool`
    :param clear_report_on_search: Whether or not an Advanced Search Query's LiveReport should have all rows removed before executing a search.

    :type conditions: :class:`list` of :class:`BaseQueryCondition`
    :param conditions: A list of query conditions.

    :type expression: :class:`str`
    :param expression: A query expression.
        - Query expressions must contain the ids of all query conditions given.
        - Query expressions use the "|" character for OR, and the "&" character for AND.
        - Grouped conditions use the "()" characters
        - Queries with a query expression containing "|" or "()" must have a view_type of QueryViewType.COMPLEX
        Examples:
            - "1 & 2 & 3 & 4"
            - "1 | 2 & 3 & 4"
            - "(1 & 2) | (3 & 4)"

    :type view_type: :class:`~experimental_enums.QueryViewType`
    :param view_type: The type of view for the query expression in the GUI.

    :type compound_return_type: :class:`~experimental_enums.QueryCompoundReturnType`
    :param compound_return_type: The type of compound entities to return in a query.

    :type frequency: :class:`~experimental_enums.QueryFrequency`
    :param frequency: The regularity with which to execute an active query.

    :type entity_return_type: :class:`list` of :class:`~experimental_enums.QueryEntityReturnType`
    :param entity_return_type: A list of the types of entities to return in a query.

    :type data_source: :class:`QueryDataSource`
    :param data_source: A data source query condition.
    """
    _fields = [
        'live_report_id', 'id', 'active', 'auto_update_results', 'clear_report_on_search',
        'conditions', 'expression', 'frequency', 'view_type', 'compound_return_type',
        'entity_return_type', 'data_source'
    ]

    def __init__(self,
                 live_report_id,
                 id=None,
                 active=False,
                 auto_update_results=False,
                 clear_report_on_search=False,
                 conditions=None,
                 expression="",
                 frequency=QueryFrequency.PERIODIC,
                 view_type=QueryViewType.SIMPLE,
                 compound_return_type=QueryCompoundReturnType.BOTH,
                 entity_return_type=[
                     QueryEntityReturnType.FORMULATION, QueryEntityReturnType.COMPOUND,
                     QueryEntityReturnType.FRAGMENT
                 ],
                 data_source=None):
        self.live_report_id = live_report_id
        self.id = id
        self.active = active
        self.auto_update_results = auto_update_results
        self.clear_report_on_search = clear_report_on_search
        self.conditions = conditions or []
        self.expression = expression
        self.frequency = frequency
        self.view_type = view_type
        self.compound_return_type = compound_return_type
        self.entity_return_type = entity_return_type
        self.data_source = data_source or QueryDataSource()

    def serialize_data_source(self):
        """
        Serializes a query data source.

        :rtype: :class:`dict`
        :return: A query data source dictionary
        """
        return QueryDataSource.as_dict(self.data_source)

    @staticmethod
    def deserialize_data_source(data_source):
        """
        Deserializes a query data source.

        :type data_source: :class:`dict`
        :param data_source: A data source dictionary

        :rtype: :class`QueryDataSource`
        :return: A query data source object
        """
        return QueryDataSource.from_dict(data_source)

    def serialize_conditions(self):
        """
        Serialize query conditions.

        :rtype: :class:`list` of :class:`dict`
        :return: A list of query condition dictionaries
        """
        return BaseQueryCondition.as_list(self.conditions)

    @staticmethod
    def deserialize_conditions(conditions):
        """
        Deserializes query conditions.

        :type conditions: :class:`list` of :class:`str`
        :param conditions: A list of query condition strings

        :rtype: :class:`list` of :class:`BaseQueryCondition`
        :return: A list of query condition objects
        """
        return BaseQueryCondition.from_list(conditions)


class Scaffold(BaseModel):
    """
    Represents a LiveDesign Scaffold.

    :type id: :class:`str`
    :param id: The ID of the Scaffold.

    :type mrv: :class:`str`
    :param mrv: A ChemAxon Marvin document.

    :type cxsmarts: :class:`str`
    :param cxsmarts: A ChemAxon Extended SMARTS string.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column id for the scaffold

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: Which project ids in which the scaffold column exists.
    """
    _fields = ['id', 'mrv', 'cxsmarts', 'addable_column_id', 'project_ids']

    def __init__(self, id=None, mrv=None, cxsmarts=None, addable_column_id=None, project_ids=None):
        self.id = id
        self.mrv = mrv
        self.cxsmarts = cxsmarts
        self.addable_column_id = addable_column_id
        self.project_ids = project_ids
