"""
The qiprofile clinical Mongodb data model.
"""

import re
import mongoengine
from mongoengine import (fields, ValidationError)
from .. import choices
from .encounter import Encounter

POS_NEG_CHOICES = [(True, 'Positive'), (False, 'Negative')]
"""The Boolean choices for Positive/Negative display values."""

YES_NO_CHOICES = [(True, 'Yes'), (False, 'No')]
"""The Boolean choices for Yes/No display values."""


class Treatment(mongoengine.EmbeddedDocument):
    """
    The patient therapy, e.g. adjuvant. Treatment is one of
    the :const:`Treatment.TYPE_CHOICES` types, and occurs over
    a period of time. The treatment consists of dosages, which
    may be pharmocological or radiological.
    """

    TYPE_CHOICES = ('Neoadjuvant', 'Primary', 'Adjuvant')

    treatment_type = fields.StringField(choices=TYPE_CHOICES)

    start_date = fields.DateTimeField(required=True)

    end_date = fields.DateTimeField(required=True)

    dosages = fields.ListField(
        field=mongoengine.EmbeddedDocumentField('Dosage')
    )


class Dosage(mongoengine.EmbeddedDocument):
    """The agent dosage."""

    agent = fields.EmbeddedDocumentField('Agent', required=True)
    """The administered Drug or Radiation."""

    amount = fields.FloatField(required=True)
    """
    The cumulative amount of the agent administered over the
    course of the duration, normalized by weight.
    
    For chemotherapy, the field unit is milligrams per kilogram
    (mg/kg).
    
    For radiotherapy, the field unit is Greys per kilogram (Gy/kg).
    
    Radiation fractions and daily chemotherapy dosages are not tracked.
    """

    start_date = fields.DateTimeField()
    """The first date the agent is administered."""

    duration = fields.IntField()
    """
    The span in days during which the agent is administered.
    """


class Agent(mongoengine.EmbeddedDocument):
    """A treatment agent, e.g. drug or radiation."""

    meta = dict(allow_inheritance=True)


class Drug(Agent):

    name = fields.StringField(required=True)
    """The official listed drug name."""


class Radiation(Agent):

    BEAM_TYPES = ['photon', 'proton', 'electron', 'neutron', 'carbon']
    """
    The radiation beam type controlled values.
    """

    beam_type = fields.StringField(choices=BEAM_TYPES)


class OtherAgent(Agent):

    name = fields.StringField(required=True)


class Biopsy(Encounter):
    """Non-therapeutic tissue extraction resulting in a pathology report."""

    pathology = fields.EmbeddedDocumentField('Pathology', required=True)


class Surgery(Encounter):
    """Therapeutic tissue extraction which usually results in a pathology report."""

    meta = dict(allow_inheritance=True)

    pathology = fields.EmbeddedDocumentField('Pathology')


class Assessment(Encounter):
    """Generic collection of outcomes."""

    evaluation = fields.EmbeddedDocumentField('GenericEvaluation')


class BreastSurgery(Surgery):
    """Breast tumor extraction."""

    TYPE_CHOICES = ('Total Mastectomy', 'Partial Mastectomy', 'Lumpectomy')
    """The surgery type controlled values."""

    surgery_type = fields.StringField(choices=TYPE_CHOICES)


class Evaluation(mongoengine.EmbeddedDocument):
    """The patient evaluation holds outcomes."""

    meta = dict(allow_inheritance=True)


class GenericEvaluation(Evaluation):
    """An unconstrained set of outcomes."""

    outcomes = fields.ListField(fields.EmbeddedDocumentField('Outcome'))


class Pathology(Evaluation):
    """The patient pathology summary."""

    meta = dict(allow_inheritance=True)

    tnm = fields.EmbeddedDocumentField('TNM')


class BreastGeneticExpression(mongoengine.EmbeddedDocument):
    """The breast patient genetic expression results."""

    HER2_NEU_IHC_CHOICES = [(0, '0'), (1, '1+'), (2, '2+'), (3, '3+')]
    """The HER2 NEU IHC choices are displayed as 0, 1+, 2+, 3+."""

    class KI67Field(fields.IntField):
        def validate(self, value, clean=True):
            return value >= 0 and value <= 100

    her2_neu_ihc = fields.IntField(choices=HER2_NEU_IHC_CHOICES)

    her2_neu_fish = fields.BooleanField(choices=POS_NEG_CHOICES)

    ki67 = KI67Field()

    normalized_assay = fields.EmbeddedDocumentField('BreastNormalizedAssay')


class BreastPathology(Pathology):
    """The QIN breast patient pathology summary."""

    hormone_receptors = fields.ListField(
        field=mongoengine.EmbeddedDocumentField('HormoneReceptorStatus')
    )

    genetic_expression = fields.EmbeddedDocumentField('BreastGeneticExpression')


class BreastNormalizedAssayField(fields.IntField):
    """The normalized Breast genomics result in the inclusive range [0, 15]."""

    def validate(self, value, clean=True):
        return value > 0 and value <= 15


class BreastNormalizedAssay(mongoengine.EmbeddedDocument):
    """The Breast genomics panel normalized to reference genes."""

    class HER2(mongoengine.EmbeddedDocument):
        grb7 = BreastNormalizedAssayField()
        her2 = BreastNormalizedAssayField()

    class Estrogen(mongoengine.EmbeddedDocument):
        er = BreastNormalizedAssayField()
        pgr = BreastNormalizedAssayField()
        bcl2 = BreastNormalizedAssayField()
        scube2 = BreastNormalizedAssayField()

    class Proliferation(mongoengine.EmbeddedDocument):
        ki67 = BreastNormalizedAssayField()
        stk15 = BreastNormalizedAssayField()
        survivin = BreastNormalizedAssayField()
        ccnb1 = BreastNormalizedAssayField()
        mybl2 = BreastNormalizedAssayField()

    class Invasion(mongoengine.EmbeddedDocument):
        mmp11 = BreastNormalizedAssayField()
        ctsl2 = BreastNormalizedAssayField()

    gstm1 = BreastNormalizedAssayField()

    cd68 = BreastNormalizedAssayField()

    bag1 = BreastNormalizedAssayField()

    her2 = fields.EmbeddedDocumentField(HER2)

    estrogen = fields.EmbeddedDocumentField(Estrogen)

    proliferation = fields.EmbeddedDocumentField(Proliferation)

    invasion = fields.EmbeddedDocumentField(Invasion)


class SarcomaPathology(Pathology):
    """The QIN sarcoma patient pathology summary."""

    HISTOLOGY_CHOICES = ('Carcinosarcoma', 'Cerebellar', 'Chondrosarcoma',
                         'Clear Cell', 'Dermatofibrosarcoma', 'Fibrosarcoma',
                         'Leiomyosarcoma', 'Liposarcoma', 'MFH', 'MPNST',
                         'Osteosarcoma', 'Rhabdomyosarcoma', 'Synovial', 'Other')
    """The histololgy controlled values."""

    location = fields.StringField()

    histology = fields.StringField(choices=HISTOLOGY_CHOICES)

    necrosis_percent = fields.EmbeddedDocumentField('NecrosisPercent')


## Clinical metrics ##

class Outcome(mongoengine.EmbeddedDocument):
    """The patient clinical outcome."""

    meta = dict(allow_inheritance=True)


class TNM(Outcome):
    """
    The TNM tumor staging. The TNM fields are as follows:

      * size - primary tumor size (T)

      * lymph_status - regional lymph nodes (N)

      * metastasis - distant metastasis (M)

      * grade - tumor grade (G)

      * serum_tumor_markers (S)

      * resection_boundaries (R)

      * lymphatic_vessel_invasion (L)

      * vein_invasion (V)

    The size is an aggregate Size field.
    See http://www.cancer.gov/cancertopics/factsheet/detection/staging for
    an overview. See http://en.wikipedia.org/wiki/TNM_staging_system and
    http://cancerstaging.blogspot.com/ for the value definition.

    :Note: The size and lymph_status choices can be further constrained by
        tumor type. Since :class:`TNM` is a generic class, these constraints
        are not enforced in this TNM class. Rather, the REST client is
        responsible for enforcing additional choice constraints. The
        :meth:`TNM.lymph_status_choices` helper method can be used for
        tumor type specific choices. See :class:`TNM.Size`` for a discussion
        of the size constraints.
  """
    class Size(mongoengine.EmbeddedDocument):
        """
        The TNM primary tumor size field.

        :Note: The size score choices can be further constrained by tumor
            type. For example, the sarcoma tumor_size choices are 0, 1 or 2
            and suffix choices are ``a`` or ``b``. See :class:`TNM` for a
            discussion of choice constraints. The :meth:`TNM.Size.tumor_size_choices`
            and :meth:`TNM.Size.suffix_choices` helper methods can be used for
            tumor type specific choices.
        """

        PREFIXES = ['c', 'p', 'y', 'r', 'a', 'u']

        SUFFIXES = ['a', 'b', 'c']

        SUFFIX_CHOICES = dict(
            Any=['a', 'b', 'c'],
            Sarcoma=['a', 'b']
        )

        TUMOR_SIZE_CHOICES = dict(
            Any=range(0, 5),
            Sarcoma=range(0, 3)
        )

        SIZE_PAT = """
            ^(
             (?P<prefix>c|p|y|r|a|u)?   # The prefix modifier
             T)?                        # The size designator
            (x |                        # Size cannot be evaluated
             (?P<in_situ>is) |          # Carcinoma in situ
             ((?P<tumor_size>0|1|2|3|4) # The size
              (?P<suffix>a|b|c)?        # The suffix modifier
             )
            )$
        """
        """
        The tumor size pattern.
        
        Examples:
        * ``T3``
        * ``pT2`` - pathology prefix
        * ``T3a`` - ``a``, ``b`` or ``c`` suffix modifier is allowed
        * ``3a`` - ``T`` prefix is optional for the size alone
        * ``Tx`` - tumor size cannot be evaluated
        * ``Tis`` - in situ
        """

        SIZE_REGEX = re.compile(SIZE_PAT, re.VERBOSE)
        """The :const:`SIZE_PAT` pattern regular expression."""

        @staticmethod
        def tumor_size_choices(tumor_type=None):
            """
            :param tumor_type: the optional tumor type, e.g. ``Breast``
            :return: the tumor_size choices for the given type
            """
            if tumor_type not in TNM.Size.TUMOR_SIZE_CHOICES:
                tumor_type = 'Any'

            return TNM.Size.TUMOR_SIZE_CHOICES[tumor_type]

        @staticmethod
        def suffix_choices(tumor_type=None):
            """
            :param tumor_type: the optional tumor type, e.g. ``Breast``
            :return: the suffix choices for the given type
            """
            if tumor_type not in TNM.Size.SUFFIX_CHOICES:
                tumor_type = 'Any'

            return TNM.Size.SUFFIX_CHOICES[tumor_type]

        prefix = fields.StringField(choices=PREFIXES)

        tumor_size = fields.IntField(choices=TUMOR_SIZE_CHOICES['Any'])

        class InSitu(mongoengine.EmbeddedDocument):
            INVASIVE_TYPE_CHOICES = ('ductal', 'lobular')
            """
            The advisory invasive types list. The client should constraion the invasive
            type choices to this list where possible, but allow for free-form text where
            necessary.
            """

            invasive_type = fields.StringField()

        in_situ = fields.EmbeddedDocumentField(InSitu)

        suffix = fields.StringField(choices=SUFFIX_CHOICES['Any'])

        def __str__(self):
            prefix = self.prefix or ''
            suffix = self.suffix or ''
            if self.in_situ:
                size = 'is'
            elif self.tumor_size:
                size = str(self.tumor_size)
            else:
                size = 'x'

            return "%sT%s%s" % (prefix, size, suffix)

        @classmethod
        def parse(klass, value):
            """
            Parses the given string into a new Size. The size must match
            the :const:`SIZE_REGEX` regular expression.

            :param value: the input string
            :return: the new Size object
            :raise ValidationError: it the size value string does not
                match :const:`SIZE_REGEX`
            """
            match = klass.SIZE_REGEX.match(value)
            if not match:
                raise ValidationError("TNM Size value is not supported:"
                                      " %s" % value)
            
            return klass(**match.groupdict())

        def clean(self):
            """
            Peforms document-level validation.

            :raise ValidationError: if the in_situ flag is set but there
              is a tumor_size or suffix field
            """
            if self.in_situ:
                if self.tumor_size != None:
                    raise ValidationError("TNM Size with in_situ flag set to"
                                          " True cannot have tumor_size %d" %
                                          self.tumor_size)
                if self.suffix != None:
                    raise ValidationError("TNM Size with in_situ flag set to"
                                          " True cannot have a suffix %s" %
                                          self.suffix)
            return True

    LYMPH_STATUS_CHOICES = dict(
        Any=range(0, 4),
        Sarcoma=range(0, 2)
    )

    tumor_type = fields.StringField(required=True)

    size = fields.EmbeddedDocumentField(Size)

    # TODO - make lymph status an aggregate with suffix modifiers,
    # including 'mi'.
    lymph_status = fields.IntField(choices=LYMPH_STATUS_CHOICES['Any'])

    metastasis = fields.BooleanField(choices=POS_NEG_CHOICES)

    grade = fields.EmbeddedDocumentField('Grade')

    serum_tumor_markers = fields.IntField(choices=range(0, 4))

    resection_boundaries = fields.IntField(choices=range(0, 3))

    lymphatic_vessel_invasion = fields.BooleanField()

    vein_invasion = fields.IntField(choices=range(0, 3))

    @staticmethod
    def lymph_status_choices(tumor_type=None):
        """
        :param tumor_type: the optional tumor type, e.g. ``Breast``
        :return: the lymph_status choices for the given type
        """
        if tumor_type not in TNM.LYMPH_STATUS_CHOICES:
            tumor_type = 'Any'

        return TNM.LYMPH_STATUS_CHOICES[tumor_type]


class Grade(Outcome):
    """
    The abstract tumor grade superclass, specialized for each
    tumor type.
    """

    meta = dict(allow_inheritance=True)


class ModifiedBloomRichardsonGrade(Grade):
    """
    The `Modified Bloom Richardson <http://pathology.jhu.edu/breast/grade.php>`_
    (a.k.a. Nottingham) breast tumor grade.
    """

    COMPONENT_CHOICES = range(1, 4)

    tubular_formation = fields.IntField(choices=COMPONENT_CHOICES)

    nuclear_pleomorphism = fields.IntField(choices=COMPONENT_CHOICES)

    mitotic_count = fields.IntField(choices=COMPONENT_CHOICES)


class HormoneReceptorStatus(Outcome):
    """The patient estrogen/progesterone hormone receptor status."""

    class IntensityField(fields.IntField):
        def validate(self, value, clean=True):
            return value > 0 and value <= 100

    hormone = fields.StringField(required=True)

    positive = fields.BooleanField(choices=YES_NO_CHOICES)

    quick_score = fields.IntField(choices=range(0, 9))

    intensity = IntensityField()


class FNCLCCGrade(Grade):
    """The FNCLCC sarcoma tumor grade."""

    differentiation = fields.IntField(choices=range(1, 4))

    necrosis_score = fields.IntField(choices=range(0, 3))

    mitotic_count = fields.IntField(choices=range(1, 4))


def necrosis_percent_as_score(necrosis_percent):
    """
    Calculates the necrosis score from the necrosis percent
    according to the
    `Stanford Synovial Sarcoma Guideline<http://surgpathcriteria.stanford.edu/softmisc/synovial_sarcoma/grading.html>`
    as follows:
    * If the percent is None, then None
    * Otherwise, if the percent is 0, then 0
    * Otherwise, if the percent is less than 50, then 1
    * Otherwise, 2
    
    :param necrosis_percent: the integer percent,
        :class:`NecrosisPercentValue` or  :class:`NecrosisPercentRange`
    :return: the necrosis score
    :raise ValidationError: if the percent is a range that spans 50%
    """
    if necrosis_percent == None:
        return None
    # Wrap a simple integer as a trivial range.
    if isinstance(necrosis_percent, int):
        necrosis_range = NecrosisPercentRange(
            start=NecrosisPercentRange.LowerBound(value=necrosis_percent),
            stop=NecrosisPercentRange.UpperBound(value=necrosis_percent + 1)
        )
    # Convert a value to a trivial range for convenience.
    elif isinstance(necrosis_percent, NecrosisPercentValue):
        necrosis_range = NecrosisPercentRange(
            start=NecrosisPercentRange.LowerBound(value=necrosis_percent.value),
            stop=NecrosisPercentRange.UpperBound(value=necrosis_percent.value + 1)
        )
    elif isinstance(necrosis_percent, NecrosisPercentRange):
        necrosis_range = necrosis_percent
    else:
        raise ValidationError("Necrosis percent type is not supported: %s" %
                              necrosis_percent.__class__)
    if necrosis_range.stop.value == 1:
        return 0
    elif necrosis_range.stop.value <= 50:
        return 1
    elif necrosis_range.start.value >= 50:
        return 2
    else:
        raise ValidationError("The necrosis percent score cannot be"
                              " determined from the range" % necrosis_range)


class NecrosisPercent(Outcome):
    """The necrosis percent value or range."""

    meta = dict(allow_inheritance=True)


class NecrosisPercentValue(NecrosisPercent):
    """The necrosis percent absolute value."""

    value = fields.IntField(choices=range(0, 101))


class NecrosisPercentRange(NecrosisPercent):
    """
    The necrosis percent range.
    
    :Note: it is recommended, although not required, that the percent
        range is a decile range, e.g. [20-30].
    
    :Note: A range which spans 50%, e.g. [40-60], results in a
        :meth:`necrosis_percent_as_score` ValidationError.
    """

    class Bound(mongoengine.EmbeddedDocument):
        """
        Necrosis percent upper or lower bound abstract class.
        The subclass is responsible for adding the ``inclusive``
        field.
        """

        meta = dict(allow_inheritance=True)

        value = fields.IntField(choices=range(0, 101))


    class LowerBound(Bound):
        """Necrosis percent lower bound."""

        inclusive = fields.BooleanField(default=True)


    class UpperBound(Bound):
        """Necrosis percent upper bound."""

        inclusive = fields.BooleanField(default=False)

    start = fields.EmbeddedDocumentField(LowerBound)

    stop = fields.EmbeddedDocumentField(UpperBound)
    
    def __repr__(self):
        return "%d-%d" % (self.start, self.stop)
