from __future__ import unicode_literals

import copy
import json
import warnings

from six import text_type as str


def ensure_string_list(list_to_convert):
    return [str(potential_integer) for potential_integer in list_to_convert]

class ModelEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, BaseModel):
            return obj.as_dict()
        else:
            return json.JSONEncoder.default(self, obj)
        
class BaseModel(object):
    _fields = []

    def as_dict(self):
        """
        :return: The instance as a dict
        """
        data = {}
        for field in self._fields:
            method = 'serialize_{0}'.format(field)
            if hasattr(self, method):
                value = getattr(self, method)()
            else:
                value = getattr(self, field)

            data[field] = value

        return data

    def as_json(self, **kwargs):
        """
        :return: The instance as a Json string
        """
        return json.dumps(self.as_dict(), **kwargs)

    @classmethod
    def as_list(self, models):
        return [model.as_dict() for model in models or []]

    @classmethod
    def from_dict(cls, data):
        local_data = {}
        for field in cls._fields:
            if field in data:
                field_data = copy.deepcopy(data[field])
                method = 'deserialize_{0}'.format(field)
                if hasattr(cls, method):
                    local_data[field] = getattr(cls, method)(field_data)
                else:
                    local_data[field] = field_data
        return cls(**local_data)

    @classmethod
    def from_json(cls, data):
        return cls.from_dict(json.loads(data))

    @classmethod
    def from_list(cls, arr):
        return [cls.from_dict(elem) for elem in arr]


class Project(BaseModel):

    _fields = [
        'id', 'name', 'active', 'alternate_id', 'default_template_id', 'description', 'restricted',
        'keep_styles', 'therapeutic_area', 'project_lead'
    ]

    def __init__(self,
                 id=None,
                 name=None,
                 active='Y',
                 alternate_id=None,
                 default_template_id=0,
                 description=None,
                 restricted=False,
                 keep_styles=False,
                 therapeutic_area=None,
                 project_lead=None):
        self.id = id
        self.name = name
        self.active = active
        self.alternate_id = alternate_id if alternate_id is not None else name
        self.default_template_id = default_template_id
        self.description = description
        self.restricted = restricted
        self.keep_styles = keep_styles
        self.therapeutic_area = therapeutic_area
        self.project_lead = project_lead


class AssayType(BaseModel):
    """ Each column in a report is an assay type. """

    _fields = ['name', 'count', 'addable_column_id', 'value_type', 'column_type', 'project_ids']

    def __init__(self,
                 name,
                 count=None,
                 addable_column_id=None,
                 value_type=None,
                 column_type=None,
                 project_ids=None):
        self.name = name
        self.count = count
        self.addable_column_id = addable_column_id
        self.value_type = value_type
        self.column_type = column_type
        self.project_ids = project_ids


class Assay(BaseModel):
    """ An assay contains at least one type, and may contain a url"""

    _fields = [
        'protocol', 'name', 'database_name', 'project_ids', 'units', 'lot', 'concentration_units',
        'concentration', 'types', 'folder_path'
    ]

    def __init__(self,
                 name,
                 types,
                 protocol=None,
                 database_name=None,
                 project_ids=None,
                 units=None,
                 lot=None,
                 concentration_units=None,
                 concentration=None,
                 folder_path=None):
        self.protocol = protocol
        self.name = name
        self.database_name = database_name
        self.project_ids = project_ids
        self.units = units
        self.lot = lot
        self.concentration_units = concentration_units
        self.concentration = concentration
        self.types = types
        self.folder_path = folder_path

    def serialize_types(self):
        return [array_type.as_dict() for array_type in self.types]

    @staticmethod
    def deserialize_types(types):
        return AssayType.from_list(types)


class DataSourceAssayFolderRules(BaseModel):
    """
    A DataSourceAssayFolderRules object contains a data source id and a list of individual AssayFolderRule, the last
    rule in that list must be a default catch-all rule

    'id': Each DataSourceAssayFolderRules object has a unique numeric id assigned to it.
    'data_source_id': Specifies the id of the data source for which the list of rules are applicable
    'rules': List of model:AssayFolderRule
    """

    _fields = ['id', 'data_source_id', 'rules']

    def __init__(self,
                 id=None,
                 data_source_id=None,
                 rules=None):
        self.id = id
        self.data_source_id = data_source_id
        self.rules = rules


class AssayFolderRule(BaseModel):
    """
    An assay folder rule is used to determine the folder into which an Assay would be placed in the column tree. The
    folder is determined by going through a list of such rules, the FIRST rule(the priority of the rule is determined by
    its position in the list with a higher priority for a rule which occurs before another rule in the list) that
    matches for an Assay is used for picking the folder. The last rule in those set of rules is a default catch-all rule
    which matches all the Assays.

    'id': Each assay folder rule has a unique numeric id assigned to it.
    'match_type': String with supported values ['STRING', 'DATE']
            Each value specifies type of the assay folder rule
            (*Must be STRING for the default catch-all rule)
    'comparison_type': String with supported values ['EQUALS', 'EQUALS_IGNORE_CASE', 'STARTS_WITH', 'ENDS_WITH',
            'INDEX_OF', 'LAST_INDEX_OF', 'REGEX', 'STARTS_WITH_LIST', 'ENDS_WITH_LIST']
            Each value specifies the match operator used for comparison to determine if an Assay satisfies the given
            Assay folder rule.
            (*Must be STARTS_WITH for the default catch-all rule)
    'match_operand': Specifies the match criteria used for a specific Assay folder rule, you can use a pipe
            character '|' to define several match criteria. eg - 'IV | PO'
            (*Must be "" for the default catch-all rule)
    'folder': Specifies the folder where the assay would be placed if it matches the given Assay folder rule. Use a
            forward slash '/' to define nested folders. eg - '/Assay/PK/Mouse'
    """

    _fields = ['id', 'match_type', 'comparison_type', 'match_operand', 'folder']

    def __init__(self,
                 id=None,
                 match_type=None,
                 comparison_type=None,
                 match_operand=None,
                 folder=None):
        self.id = id
        self.match_type = match_type
        self.comparison_type = comparison_type
        self.match_operand = match_operand
        self.folder = folder


class AssayDefaultAggregationRule(BaseModel):
    """
    An assay default aggregation rule is used to determine the 'default' aggregation mode of an Assay till it is
    explicitly overriden by the user. The default aggregation mode of an Assay is determined by going through a list of
    such rules, the FIRST rule(the priority of the rule is determined by its position in the list with a higher priority
    for a rule which occurs before another rule in the list) that matches for an Assay is used for picking the default
    aggregation mode. In case none of the rules match, there is a server wide rule(the last rule in the list) which
    would be used which matches all the Assays.

    'id': Each assay default aggregation rule has a unique numeric id assigned to it.
    'match_target': String with supported values ['ASSAY_RESULT_TYPE', 'ASSAY_RESULT_NAME']
            Each value specifies whether to match an assay using its name or type
            (*Must be None for the server wide rule)
    'match_operator': String with supported values ['EQUALS', 'EQUALS_IGNORE_CASE', 'STARTS_WITH', 'ENDS_WITH',
            'INDEX_OF', 'LAST_INDEX_OF', 'REGEX', 'STARTS_WITH_LIST', 'ENDS_WITH_LIST']
            Each value specifies the match operator used for comparison to determine if an Assay satisfies the given
            Assay default aggregation rule.
            (*Must be None for the server wide rule)
    'match_operand': Specifies the match criteria used for a specific Assay default aggregation rule, you can use a pipe
            character '|' to define several match criteria. eg - 'IV | PO'
            (*Must be None for the server wide rule)
    'method': String with supported values ['median', 'arithmetic_mean', 'min', 'max', 'geometric_mean', 'std_dev',
            'count', 'latest', 'unaggregated']
            Each value specifies the cell aggregation type to be used for an assay matching the given rule
    'exclude_operators': Boolean value which sets whether the operators will be excluded from the calculation of the
            assay result.

    EXAMPLE: {"ASSAY_RESULT_TYPE", "EQUALS", "Ki", "geometric_mean", "true"}
            Results in all Assays with a result type of "Ki" displaying an assay result that is the geometric mean of
            the values without operators.

             {"ASSAY_RESULT_TYPE", "EQUALS", "EC50", "arithmetic_mean", "false"}
            Results in all Assays with a result type of "EC50" displaying an assay result that is the arithmetic mean of
            the values with operators.
    """

    _fields = ['id', 'match_target', 'match_operator', 'match_operand', 'method', 'exclude_operators']

    def __init__(self,
                 id=None,
                 match_target=None,
                 match_operator=None,
                 match_operand=None,
                 method=None,
                 exclude_operators=None):
        self.id = id
        self.match_target = match_target
        self.match_operator = match_operator
        self.match_operand = match_operand
        self.method = method
        self.exclude_operators = exclude_operators


class Column(BaseModel):

    _fields = ['id', 'name', 'column_type', 'value_type', 'log_scale', 'project_ids', 'folder_name']

    def __init__(self,
                 id=None,
                 name=None,
                 column_type=None,
                 value_type=None,
                 log_scale=None,
                 project_ids=None,
                 folder_name=None):
        self.id = id
        self.name = name
        self.column_type = column_type
        self.value_type = value_type
        self.log_scale = log_scale
        self.project_ids = project_ids
        self.folder_name = folder_name


# move backup above assaytype
class Rationale(BaseModel):

    _fields = [
        'description', 'user_name', 'corporate_id', 'live_report_id', 'id', 'default', 'created_at',
        'updated_at'
    ]

    def __init__(self,
                 description="",
                 user_name="",
                 corporate_id=None,
                 live_report_id=None,
                 default=None,
                 id=None,
                 created_at=None,
                 updated_at=None):
        self.description = description
        self.user_name = user_name
        self.corporate_id = corporate_id
        self.live_report_id = live_report_id
        self.default = default
        self.id = id
        self.created_at = created_at
        self.updated_at = updated_at


class Database(BaseModel):

    _fields = ['name', 'projects', 'ideas', 'writeback', 'primary']

    def __init__(self, name, projects=None, ideas=True, primary=True, writeback=True):
        self.name = name
        self.projects = projects
        self.primary = primary
        self.writeback = writeback
        self.ideas = ideas


class DatabaseColumn(BaseModel):

    _fields = ['id', 'name', 'addable_column_id', 'value_type', 'folder_path']

    def __init__(self, id, name, addable_column_id, value_type, folder_path):
        self.id = id
        self.name = name
        self.addable_column_id = addable_column_id
        self.value_type = value_type
        self.folder_path = folder_path


class LiveReport(BaseModel):
    NEVER = 'never'
    BY_CACHEBUILDER = 'by_cachebuilder'
    WHEN_OPENED = 'when_opened'
    COMPOUND = 'compound'
    REACTANT = 'reactant'

    def __init__(self,
                 title,
                 description='',
                 update_policy=BY_CACHEBUILDER,
                 template=False,
                 shared_editable=True,
                 default_rationale=None,
                 assay_view=None,
                 id=None,
                 alias=None,
                 project_id=0,
                 owner=None,
                 is_private=False,
                 active=True,
                 addable_columns=None,
                 tags=None,
                 last_saved_date=None,
                 sorted_columns=None,
                 hidden_rows=None,
                 scaffolds=None,
                 report_level='parent',
                 type=COMPOUND):
        self.title = title
        self.description = description
        self.update_policy = update_policy
        self.type = type
        self.template = template
        self.shared_editable = shared_editable
        self.default_rationale = default_rationale
        self.assay_view = assay_view
        self.id = id
        self.alias = alias
        self.project_id = str(project_id)
        self.owner = owner
        self.is_private = is_private
        self.active = active
        self.addable_columns = addable_columns
        self.tags = tags if tags else []
        self.last_saved_date = last_saved_date
        self.sorted_columns = sorted_columns if sorted_columns else []
        self.hidden_rows = hidden_rows if hidden_rows else []
        self.scaffolds = scaffolds if scaffolds else []
        self.report_level = report_level

    def as_json_string(self, standard_required=True):
        return json.dumps(self.as_dict(standard_required=standard_required))

    # standard_required determines whether the field should always have an entry in the dict.  This is useful for fields
    # which are required to represent a live_report, compared to those which add supplementary details
    def as_dict(self, standard_required=True):
        data = {}
        if self.title or standard_required:
            data['title'] = self.title
        if self.description or standard_required:
            data['description'] = self.description
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.last_saved_date or standard_required:
            data['last_saved_date'] = self.last_saved_date
        if self.sorted_columns or standard_required:
            data['sorted_columns'] = self.sorted_columns
        if self.hidden_rows or standard_required:
            data['hidden_rows'] = self.hidden_rows
        if self.scaffolds or standard_required:
            data['scaffolds'] = self.scaffolds
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.addable_columns:
            data['addable_columns'] = self.addable_columns
        if self.tags or standard_required:
            data['tags'] = self.tags
        if self.update_policy or standard_required:
            data['update_policy'] = self.update_policy
        if self.type or standard_required:
            data['type'] = self.type
        if self.template or standard_required:
            data['template'] = self.template
        if self.is_private or standard_required:
            data['is_private'] = self.is_private
        if self.active or standard_required:
            data['active'] = self.active
        if self.shared_editable or standard_required:
            data['shared_editable'] = self.shared_editable
        if self.default_rationale:
            data['default_rationale'] = self.default_rationale
        if self.assay_view:
            data['assay_view'] = self.assay_view.as_dict()
        if self.id or standard_required:
            data['id'] = self.id
        if self.report_level or standard_required:
            data['report_level'] = self.report_level
        if self.alias:
            data['alias'] = self.alias
        if self.project_id:
            data['project_id'] = self.project_id
        return data

    @staticmethod
    def from_dict(data):
        addable_columns = data.get('addable_columns')
        alias = data.get('alias')

        if data.get('assay_view'):
            assay_view = ViewMap.from_dict(data['assay_view'])
        else:
            assay_view = None

        default_rationale = data.get('default_rationale')

        return LiveReport(
            title=data['title'],
            description=data['description'],
            owner=data['owner'],
            is_private=data['is_private'],
            update_policy=data['update_policy'],
            type=data['type'],
            template=data['template'],
            active=data['active'],
            shared_editable=data['shared_editable'],
            addable_columns=addable_columns,
            tags=data['tags'],
            last_saved_date=data['last_saved_date'],
            sorted_columns=data['sorted_columns'],
            hidden_rows=data['hidden_rows'],
            default_rationale=default_rationale,
            assay_view=assay_view,
            id=data['id'],
            alias=alias,
            project_id=data.get('project_id'),
            scaffolds=data.get('scaffolds'),
            report_level=data.get('report_level'))

    @classmethod
    def from_list(cls, arr):
        return [cls.from_dict(elem) for elem in arr]


class LiveReportMetadata(BaseModel):
    NEVER = 'never'
    BY_CACHEBUILDER = 'by_cachebuilder'
    WHEN_OPENED = 'when_opened'
    COMPOUND = 'compound'
    REACTANT = 'reactant'

    def __init__(self,
                 title,
                 description='',
                 update_policy=BY_CACHEBUILDER,
                 template=False,
                 shared_editable=True,
                 id=None,
                 alias=None,
                 project_id=0,
                 owner=None,
                 is_private=False,
                 active=True,
                 tags=None,
                 last_saved_date=None,
                 report_level='parent',
                 type=COMPOUND):
        self.title = title
        self.description = description
        self.update_policy = update_policy
        self.type = type
        self.template = template
        self.shared_editable = shared_editable
        self.id = id
        self.alias = alias
        self.project_id = str(project_id)
        self.owner = owner
        self.is_private = is_private
        self.active = active
        self.tags = tags if tags else []
        self.last_saved_date = last_saved_date
        self.report_level = report_level

    def as_json_string(self, standard_required=True):
        return json.dumps(self.as_dict(standard_required=standard_required))

    def as_dict(self, standard_required=True):
        data = {}
        if self.title or standard_required:
            data['title'] = self.title
        if self.description or standard_required:
            data['description'] = self.description
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.last_saved_date or standard_required:
            data['last_saved_date'] = self.last_saved_date
        if self.tags or standard_required:
            data['tags'] = self.tags
        if self.update_policy or standard_required:
            data['update_policy'] = self.update_policy
        if self.type or standard_required:
            data['type'] = self.type
        if self.template or standard_required:
            data['template'] = self.template
        if self.is_private or standard_required:
            data['is_private'] = self.is_private
        if self.active or standard_required:
            data['active'] = self.active
        if self.shared_editable or standard_required:
            data['shared_editable'] = self.shared_editable
        if self.id or standard_required:
            data['id'] = self.id
        if self.report_level or standard_required:
            data['report_level'] = self.report_level
        if self.alias:
            data['alias'] = self.alias
        if self.project_id:
            data['project_id'] = self.project_id
        return data

    @staticmethod
    def from_dict(data):
        alias = data.get('alias')

        return LiveReportMetadata(
            title=data['title'],
            description=data['description'],
            owner=data['owner'],
            is_private=data['is_private'],
            update_policy=data['update_policy'],
            type=data['type'],
            template=data['template'],
            active=data['active'],
            shared_editable=data['shared_editable'],
            tags=data['tags'],
            last_saved_date=data['last_saved_date'],
            id=data['id'],
            alias=alias,
            project_id=data.get('project_id'),
            report_level=data.get('report_level'))

    @classmethod
    def from_list(cls, arr):
        return [cls.from_dict(elem) for elem in arr]


class ModelRecursive(BaseModel):
    """
    A class that tags data with how it should be inherited from parent models
    """
    _fields = ['tag', 'value']

    def __init__(self, tag, value=None):
        self.tag = tag
        self.value = value


class ModelCommand(BaseModel):
    """
    A single Commanod to be executed as part of a model
    """
    _fields = ['id', 'command', 'driver_id']

    def __init__(self, command, driver_id, id=None):
        self.id = id
        self.command = command
        self.driver_id = driver_id


class ModelAsyncDriver(BaseModel):
    """
    A Driver for handling Async Commands on Task Engine
    """
    _fields = ['id', 'name', 'job_id_regex', 'job_status']

    def __init__(self, name, job_id_regex, job_status, id=None):
        self.id = id
        self.name = name
        self.job_id_regex = job_id_regex
        self.job_status = job_status


class ModelReturn(BaseModel):
    """
    A model predictions object
    Allowed types: Real, Integer, String, Boolean
    """

    _fields = [
        'id', 'key', 'type', 'units', 'display_name', 'tag', 'precision', 'addable_column_id'
    ]

    def __init__(self,
                 key,
                 type,
                 units,
                 display_name,
                 tag='DEFAULT',
                 precision=1,
                 addable_column_id=None,
                 id=None):
        self.key = key
        self.type = type
        self.units = units
        self.tag = tag
        self.precision = precision
        self.display_name = display_name
        self.addable_column_id = addable_column_id
        self.id = id


class Model(BaseModel):
    """
    Models, Parametrizede Models and Protocols all use the same structure and table.
    """
    _fields = [
        'id', 'parent', 'name', 'archived', 'published', 'user', 'created_at', 'updated_at',
        'folder', 'returns', 'template_vars', 'batch_size', 'description', 'command_type',
        'command_queue', 'commands', 'project_ids'
    ]

    def __init__(self,
                 name,
                 archived,
                 published,
                 user,
                 folder,
                 returns,
                 project_ids,
                 template_vars,
                 description,
                 parent=None,
                 commands=None,
                 batch_size=None,
                 command_type=None,
                 command_queue=None,
                 created_at=None,
                 updated_at=None,
                 id=None):
        self.id = id
        self.parent = parent
        self.name = name
        self.archived = archived
        self.published = published
        self.user = user
        self.created_at = created_at
        self.updated_at = updated_at
        self.folder = folder
        self.returns = returns
        self.template_vars = template_vars
        self.batch_size = batch_size
        self.description = description
        self.command_type = command_type
        self.command_queue = command_queue
        self.commands = [] if not commands else commands
        self.project_ids = ["0"] if not project_ids else ensure_string_list(project_ids)

    def serialize_commands(self):
        return [cmd.as_dict() for cmd in self.commands]

    def serialize_returns(self):
        return [ret.as_dict() for ret in self.returns]

    def serialize_template_vars(self):
        if not self.template_vars:
            return []
        return [var.as_dict() for var in self.template_vars]

    def serialize_batch_size(self):
        if self.batch_size:
            return self.batch_size.as_dict()
        return None

    def serialize_command_queue(self):
        if self.command_queue:
            return self.command_queue.as_dict()
        return None

    def serialize_command_type(self):
        if self.command_type:
            return self.command_type.as_dict()
        return None

    @staticmethod
    def deserialize_commands(cmds):
        return [ModelCommand.from_dict(cmd) for cmd in cmds]

    @staticmethod
    def deserialize_returns(rets):
        return [ModelReturn.from_dict(ret) for ret in rets]

    @staticmethod
    def deserialize_template_vars(vars):
        return [ModelTemplateVar.from_dict(var) for var in vars]

    @staticmethod
    def deserialize_batch_size(d):
        if d is None:
            return None
        return ModelRecursive.from_dict(d)

    @staticmethod
    def deserialize_command_queue(d):
        if d is None:
            return None
        return ModelRecursive.from_dict(d)

    @staticmethod
    def deserialize_command_type(d):
        if d is None:
            return None
        return ModelRecursive.from_dict(d)


class ModelTemplateVar(BaseModel):
    """
    a binding of a variable in a command
    """
    _fields = ['id', 'tag', 'type', 'name', 'data']

    def __init__(self, tag, type, name, data=None, id=None):
        self.id = id
        self.tag = tag
        self.type = type
        self.name = name
        self.data = data


class QuickTestModel(BaseModel):

    _fields = [
        'model',
        'protocol',
        'compounds',
        'files',
        'column_types',
        'compound_data',
        'project_id',
        'task_engine_task_id',
        'request_id',
    ]

    def __init__(self,
                 model,
                 protocol,
                 compounds,
                 files,
                 column_types,
                 compound_data,
                 project_id,
                 task_engine_task_id=None,
                 request_id=None):
        self.model = model
        self.protocol = protocol
        self.compounds = compounds
        self.files = files
        self.column_types = column_types
        self.compound_data = compound_data
        self.project_id = project_id
        self.task_engine_task_id = task_engine_task_id
        self.request_id = request_id
        self.serialize_model = lambda: self.model.as_dict()
        self.serialize_protocol = lambda: self.protocol.as_dict()


class Folder(BaseModel):

    _fields = ['name', 'id', 'project_id']

    def __init__(self, name, id, project_id):
        """
        [{"id":"18330","name":"blc","project_id":"10"}]
        valid values for display_column_type are "computational_model" or "computed_property"
        folder_path:  "User Defined/<username>" or "Physicochemical Descriptors"
        """
        self.name = name
        self.id = id
        self.project_id = project_id


class ColumnDescriptor(BaseModel):
    """
    A column descriptor defines how the column is going to appear to the user in the LR.
    A column descriptor may also contain column filters and coloring rules.
    """
    MEDIAN = 'median'
    ARITHMETIC_MEAN = 'arithmetic_mean'
    MIN = 'min'
    MAX = 'max'
    GEOMETRIC_MEAN = 'geometric_mean'
    SYSTEM_DEFAULT = 'system_default'
    STD_DEV = 'std_dev'
    COUNT = 'count'

    _fields = [
        'column_id', 'addable_column_id', 'active_mpo_id_for_coloring', 'live_report_id', 'display_name', 'width', 'color_styles',
        'filters', 'hidden', 'aggregation_type', 'id'
    ]

    def __init__(self,
                 column_id,
                 addable_column_id=None,
                 active_mpo_id_for_coloring=None,
                 live_report_id=None,
                 display_name=None,
                 width=None,
                 color_styles=None,
                 filters=None,
                 hidden=False,
                 aggregation_type=SYSTEM_DEFAULT,
                 id=None):
        self.column_id = column_id
        if column_id and not addable_column_id:
            warnings.warn(
                'The column_id field is deprecated and will soon be removed from LD. '
                'Please also include addable_column_id to ease code migration when column_id will be removed.',
                DeprecationWarning)
        self.addable_column_id = addable_column_id
        self.live_report_id = live_report_id
        if display_name:
            warnings.warn(
                'The display_name field has been removed from LD and addable_column_id should be used instead.',
                DeprecationWarning)
        self.display_name = display_name if display_name else column_id
        self.width = width
        self.color_styles = color_styles if color_styles else []
        self.filters = filters if filters else []
        self.hidden = hidden
        self.aggregation_type = aggregation_type
        self.id = id
        self.active_mpo_id_for_coloring = active_mpo_id_for_coloring


class ColumnFilter(BaseModel):
    _fields = ['filter_type', 'enabled', 'inverted']

    def __init__(self, filter_type='text', enabled=True, inverted=False, addable_column_id=None):
        self.filter_type = filter_type
        self.enabled = enabled
        self.inverted = inverted
        self.addable_column_id = addable_column_id

    @classmethod
    def parse_from_dict(cls, data):
        if data['filter_type'] == 'text':
            return TextColumnFilter.from_dict(data)
        elif data['filter_type'] == 'range':
            return RangeColumnFilter.from_dict(data)
        else:
            raise ValueError("filter_type must be either 'text' or 'range'")


class TextColumnFilter(ColumnFilter):
    _fields = ColumnFilter._fields + ['text_match_type', 'case_sensitive', 'delimiter', 'value']

    def __init__(self,
                 filter_type='text',
                 enabled=True,
                 inverted=False,
                 addable_column_id=None,
                 text_match_type='exactly',
                 case_sensitive=False,
                 delimiter='__DEBUG__',
                 value=None):
        ColumnFilter.__init__(
            self,
            filter_type=filter_type,
            enabled=enabled,
            inverted=inverted,
            addable_column_id=addable_column_id)
        self.text_match_type = text_match_type
        self.case_sensitive = case_sensitive
        self.delimiter = delimiter
        self.value = value
        # Validation
        valid_text_match_types = ['exactly', 'prefix', 'anywhere']
        if self.text_match_type not in valid_text_match_types:
            raise ValueError("text_match_type must be one of: {}".format(
                ', '.join(valid_text_match_types)))
        if value is not None and type(value) is not str:
            self.value = str(value)


class RangeColumnFilter(ColumnFilter):
    _fields = ColumnFilter._fields + ['date', 'log_scale', 'range_high', 'range_low']

    def __init__(self,
                 filter_type='range',
                 enabled=True,
                 inverted=False,
                 addable_column_id=None,
                 date=False,
                 log_scale=False,
                 range_high=None,
                 range_low=None):
        ColumnFilter.__init__(
            self,
            filter_type=filter_type,
            enabled=enabled,
            inverted=inverted,
            addable_column_id=addable_column_id)
        self.date = date
        self.log_scale = log_scale
        self.range_high = range_high
        self.range_low = range_low


class Parameter(BaseModel):

    _fields = ['operator', 'value', 'runtime', 'display']

    def __init__(self, operator, value, runtime, display=None):
        self.operator = operator
        self.value = value
        self.runtime = runtime
        self.display = display


class ViewMap(BaseModel):

    _fields = ['color_styles']

    def __init__(self, color_styles):
        self.color_styles = color_styles if color_styles else []

    def serialize_color_styles(self):
        return [style.as_dict() for style in self.color_styles]

    @staticmethod
    def deserialize_color_styles(styles):
        return ColorStyle.from_list(styles)


class ColorStyle(BaseModel):

    _fields = [
        'color_column', 'color_low', 'color_high', 'range_low', 'range_high', 'source_column',
        'value_low', 'value_high', 'color_object', 'log_scale', 'matching_string'
    ]

    def __init__(self,
                 color_column,
                 color_low,
                 color_high,
                 range_low,
                 range_high,
                 source_column,
                 value_low,
                 value_high,
                 color_object=False,
                 log_scale=False,
                 matching_string=None):
        self.color_column = color_column
        self.color_low = color_low
        self.color_high = color_high
        self.range_low = range_low
        self.range_high = range_high
        self.source_column = source_column
        self.value_low = value_low
        self.value_high = value_high
        self.color_object = color_object
        self.log_scale = log_scale
        self.matching_string = matching_string


class LiveReportResult(BaseModel):
    """The result of executing a live report"""

    _fields = ['columns', 'results']

    def __init__(self, columns, results):
        self.columns = columns
        self.results = results


class ChangedObservation(BaseModel):
    _fields = ['sample_id', 'protocol_id', 'type_id']

    def __init__(self, sample_id, protocol_id, type_id):
        super(ChangedObservation, self).__init__()
        self.type_id = type_id
        self.sample_id = sample_id
        self.protocol_id = protocol_id


class ChangedLiveReport(BaseModel):
    _fields = ['live_report_id']

    def __init__(self, live_report_id):
        super(ChangedLiveReport, self).__init__()
        self.live_report_id = live_report_id


class RemovedLiveReportRow(BaseModel):
    _fields = ['live_report_id', 'entity_id']

    def __init__(self, live_report_id, entity_id):
        super(RemovedLiveReportRow, self).__init__()
        self.live_report_id = live_report_id
        self.entity_id = entity_id


class ChangedEntity(BaseModel):
    _fields = ['entity_id']

    def __init__(self, entity_id):
        super(ChangedEntity, self).__init__()
        self.entity_id = entity_id


class FreeformColumn(BaseModel):
    COLUMN_TEXT = "TEXT"
    COLUMN_NUMBER = "NUMBER"
    COLUMN_BOOLEAN = "BOOLEAN"
    COLUMN_DATE = "DATE"
    COLUMN_ATTACHMENT = "ATTACHMENT"

    VALUE_STRING = "string"

    _fields = [
        'id',
        'name',
        'description',
        'type',
        'value_type',
        'published',
        'live_report_id',
        'project_id',
        'picklist',
        'values',
    ]

    def __init__(self,
                 name,
                 description,
                 type=COLUMN_TEXT,
                 picklist=False,
                 values=[],
                 published=None,
                 project_id=None,
                 live_report_id=None,
                 value_type=VALUE_STRING,
                 id=None):
        picklist_values = [
            FreeformColumnPicklistValue(
                value=value) if not isinstance(value, FreeformColumnPicklistValue) else value
            for value in values
        ]

        self.id = id
        self.name = name
        self.description = description
        self.type = type
        self.value_type = value_type
        self.published = published
        self.live_report_id = live_report_id
        self.project_id = project_id
        self.picklist = picklist
        self.values = picklist_values

    def as_dict(self):
        result = super(FreeformColumn, self).as_dict()
        result['values'] = [pick_item.as_dict() for pick_item in result['values'] or []]
        return result


class FreeformColumnPicklistValue(BaseModel):
    _fields = [
        'id',
        'value',
    ]

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


class Observation(BaseModel):
    _fields = [
        'id',
        'project_id',
        'entity_id',
        'addable_column_id',
        'value',
        'units',
        'lot_number',
        'salt_id',
        'protocol',
        'date',
        'notebook',
        'notebook_page',
        'concentration',
        'concentration_units',
        'operator',
        'batch',
        'attachment_urls',
        'live_report_id',
        'published',
    ]

    def __init__(self,
                 project_id,
                 entity_id,
                 addable_column_id,
                 value,
                 live_report_id=None,
                 units=None,
                 lot_number=None,
                 salt_id=None,
                 protocol=None,
                 date=None,
                 notebook=None,
                 notebook_page=None,
                 concentration=None,
                 concentration_units=None,
                 operator=None,
                 batch=None,
                 attachment_urls=None,
                 published=None,
                 id=None):
        self.id = id
        self.project_id = project_id
        self.entity_id = entity_id
        self.addable_column_id = addable_column_id
        self.value = value
        self.units = units
        self.lot_number = lot_number
        self.salt_id = salt_id
        self.protocol = protocol
        self.date = date
        self.notebook = notebook
        self.notebook_page = notebook_page
        self.concentration = concentration
        self.concentration_units = concentration_units
        self.operator = operator
        self.batch = batch
        self.attachment_urls = attachment_urls
        self.live_report_id = live_report_id
        self.published = published


class CompoundLink(BaseModel):
    """
    Represents a compound link search result object.
    """

    _fields = ['id', 'real_entity_id', 'virtual_entity_id']

    def __init__(self, id=None, real_entity_id=None, virtual_entity_id=None):
        self.id = id
        self.real_entity_id = real_entity_id
        self.virtual_entity_id = virtual_entity_id


class ColumnAlias(BaseModel):
    """
    Represents a Column Alias search result object.
    
    'id': Id of the ColumnAlias object
    'alias': Alias name of the ColumnAlias object
    'addable_column_id': The addable column ID wrt this ColumnAlias object
    'live_report_id': The live report ID for the ColumnAlias object
    ;project_id': The project ID for the ColumnAlias object
    """

    _fields = ['id', 'alias', 'addable_column_id', 'live_report_id', 'project_id']

    def __init__(self, id=None, alias=None, addable_column_id=None, live_report_id=None, project_id=None):
        self.id = id
        self.alias = alias
        self.addable_column_id = addable_column_id
        self.live_report_id = live_report_id
        self.project_id = project_id


class SchrodingerException(Exception):

    @property
    def message(self):
        """Returns the first argument used to construct this error."""
        return self.args[0]


class InvalidArgumentException(SchrodingerException):
    pass


class AsyncServiceTaskFailedError(Exception):
    pass
