"""Helper functions for performing small tasks using the Ensembl REST API
"""
# import pprint as pp
CHR_NAME = "chr_name"
START_POS = "start_pos"
END_POS = "end_pos"

_BIOTYPE_CACHE = {}
_CHROMOSOME_CACHE = {}


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_ld(rc, chr_name, start_pos, ref_allele, alt_allele, species='human',
           population_name='1000GENOMES:phase_3:EUR', **kwargs):
    """Get variants in LD with the variant coords and alleles, within a
    certain region size.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        The rest client object.
    chr_name : `str`
        The chromosome name for the variant.
    start_pos : `int`
        The start position for the variant.
    ref_allele : `str`
        The reference allele for the variant.
    alt_allele : `str`
        The alternate allele for the variant.
    species : `str`, optional, default: `human`
        The species for the LD panel.
    species : `str`, optional, default: `1000GENOMES:phase_3:EUR`
        The population for the LD panel, default is 1000 genomes European.
    **kwargs
        Keyword aruments to `ensembl_rest_client.Rest.get_ld`. Note, the
        ``attrib`` argument is ignored and on by default.

    Returns
    -------
    ld_matches : `list` of `dict`
        Pairwise LD matches for the query variant.

    Raises
    ------
    KeyError
        If the chromosome coordinates are not associated with an rsID
    """
    # Always get the attribs
    kwargs['attribs'] = True

    # Convert the coordinate positions to an rsID, this will fail with a
    # KeyError if not possible
    var_id = get_var_id(rc, chr_name, start_pos, ref_allele, alt_allele)
    # print(var_id)
    # Get the LD data for the variant
    ld_matches = rc.get_ld(var_id['id'], population_name, species,
                           **kwargs)

    # The variant ID post data size
    post_size = 200

    var_info = {}
    # Because the LD data is not associated with any alleles, we have to use
    # the variant identifiers to get the alleles for the variants
    ids = [i['variation'] for i in ld_matches]
    for i in range(0, len(ids), post_size):
        var_info = {
            **var_info, **(rc.post_variation(species, ids[i:i+post_size]))
        }

    # Now we go through the LD variants and match up the alleles from the
    # variants that we have queried
    for i in ld_matches:
        i['source_variant'] = var_id
        try:
            for j in var_info[i['variation']]['mappings']:
                j['alleles'] = j['allele_string'].split('/')
                if j['seq_region_name'] == i['chr'] \
                   and j['start'] == i['start']:
                    i['ld_variant'] = j
                    break
        except KeyError:
            i['ld_variant'] = {}

    return ld_matches


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_var_id(rc, chr_name, start_pos, ref_allele, alt_allele,
               species='human'):
    """Get a variant rs ID from positional coords and alleles.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        The rest client object.
    chr_name : `str`
        The chromosome name for the variant.
    start_pos : `int`
        The start position for the variant.
    ref_allele : `str`
        The reference allele for the variant.
    alt_allele : `str`
        The alternate allele for the variant.
    species : `str`, optional, default: `human`
        The species for the LD panel.
    species : `str`, optional, default: `1000GENOMES:phase_3:EUR`
        The population for the LD panel, default is 1000 genomes European.
    **kwargs
        Keyword aruments to `ensembl_rest_client.Rest.get_ld`. Note, the
        ``attrib`` argument is ignored and on by default.

    Returns
    -------
    variant : `dict`
        A dictionary containing the data for the matching variant.

    Raises
    ------
    KeyError
        If the chromosome coordinates are not associated with an rsID.
    ValueError
        If the ref allele is the same as the alt allele.

    Notes
    -----
    Note that alleles are matched in a non-ref/alt way, so only ref/alt needs
    to be present in any order for the match to be valid.
    """
    if ref_allele == alt_allele:
        raise ValueError("alleles must be different")

    start_pos, end_pos, ref, alts = vcf_to_ensembl(
        start_pos, ref_allele, [alt_allele], dbcoords=False
    )
    alt_allele = alt_allele[0]

    regions = rc.get_overlap_region(
        'variation', "{0}:{1}-{2}".format(chr_name, start_pos, end_pos),
        species
    )
    for i in regions:
        if ref_allele in i['alleles'] and alt_allele in i['alleles']:
            return i

    raise KeyError("No variant for {0}:{1}|{2}|{3}".format(
        chr_name, start_pos, ref_allele, alt_allele)
    )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def vcf_to_ensembl(pos, ref, alts, dbcoords=True):
    """Convert VCF coordinates and alleles to ensembl ones, this assumes that
    everything is on the + strand.

    Parameters
    ----------
    pos : `int`
        The VCF POS column
    ref : `str`
        The VCF REF column
    alts : `list` of `str`
        The VCF alternate alleles column
    dbcoords : `bool`, optional, default: `True`
        Adjust INDEL coordinates to match the Ensembl database. In the case of
        INSERTIONS this means start>end. If False then start == end (as in the
        Ensembl GVF files).

    Returns
    -------
    start_pos : `int`
        The ensemblised start position.
    end_pos : `int`
        The ensemblised end position.
    ref_allele : `str`
        The ensemblised reference allele.
    alt_alleles : `list` of `str`
        The ensemblised alternate alleles.
    """
    blank_allele = '-'

    # Make a combined allele list for ease
    alleles = [ref] + alts

    # Make sure that pos is an integer
    pos = int(pos)

    # first make sure they are all DNA_REGEX not that this just tests for
    # ATCG or atcg deletions (-) are not in this regexp. The presence of a -
    # would be indicative of this already being an Ensembl allele
    # is_dna = [
    #     True if constants.DNA_REGEX_DEL.match(i) else False for i in alleles
    # ]
    # # if False in is_dna is True:
    # if False in is_dna:
    #     raise ensembl_errors.SequenceError(
    #         "expected DNA alleles not '{0}'".format("/".join(alleles)))

    # Now make sure it is not already in ensembl format
    is_ensembl = [True if blank_allele == i else False for i in alleles]
    if True in is_ensembl:
        raise ValueError(
            "already ensembl format '{0}'".format("/".join(alleles)))

    # Now get the lengths of the alleles, we only need to do the conversion
    # if the alleles are unbalenced (i.e. not of equal lengths)
    lengths = [len(i) for i in alleles]
    equal_len = [True if len(i) == lengths[0] else False for i in alleles]
    if all(equal_len) is True:
        # In this case we return the start/end and alleles
        # the alleles could still be balenced and > length 1 hence we take
        # the length into account for the end pos
        return pos, pos+len(alleles[0])-1, ref, alts

    # If we get here then we are unbalenced and not in Ensembl format. So in
    # a simple VCF entry we remove the leading base from all the alleles.
    # However, we could have a more complex entry where the leading base is
    # not the same in all scenarios, in these cases we make no allele
    # alterations, this mimicks the behaviour of the VEP

    # This gives the smallest length for an unbalenced VCF allele this should
    # be 1 in the simple, i.e. the common base before the variation. However,
    # Insertion/Deletions and Deletion/Insertions will be different
    smallest = min(lengths)

    # Get the index (i.e. position in the alleles of the smallest one. There is
    # a possibility of a tie for the smallest, in this case we can't convert as
    # both will start with different alleles
    smallest = [c for c, i in enumerate(lengths) if i == smallest]

    # Now we make sure that all the alleles start with te smallest allele and
    # if so we chop it off and make the smallest into a -. Howver, we actually
    # want to check the first base of the smallest allele as there are some
    # deletion insertions. i.e. in VCF TAAAA/TC. i.e. AAAA has been deleted
    # and C has been inserted in it's place
    min_smallest = min(smallest)

    # Get the sequence of the load base so we can test that it is leading in
    # all alleles
    # TODO: Do I need to account for the lead base being . or None?
    # TODO: No I don't as these will raise SequenceErrors above
    lead_base = alleles[min_smallest][0]

    # Loop through the alleles removing the leading bp  if they are present.
    # the modified allele strings are present in new_alleles
    new_alleles = []
    for i in alleles:
        if i.startswith(lead_base):
            ensembl_allele = i[1:]

            # This might make the string empty if so insert the blank allele
            if ensembl_allele == '':
                ensembl_allele = blank_allele

            new_alleles.append(ensembl_allele)
        else:
            # TODO: This is really here for debugging purposes, when I see
            # TODO: examples of these I will adjust
            # If at least one of the alleles does not start with the lead base
            # then we have a mixed variant type. I have noticed that some of
            # the HGMD variants are poorly defined
            # TODO: This will change and is for debugging the rule set
            # I have seen this
            if alleles[1] is None and len(alleles[0]) == 1:
                raise ValueError("Can't convert orphan alleles")
            # print(pos, file=sys.stderr)
            # print(pp.pformat(is_dna), file=sys.stderr)
            # print(False in is_dna, file=sys.stderr)
            # print(pp.pformat(alleles), file=sys.stderr)
            # print(alleles[1], file=sys.stderr)
            # print(pp.pformat(lengths), file=sys.stderr)
            raise RuntimeError("Can't convert mixed variation types")

    # If we get here, then everything has the same lead base
    # Now deal with the coordinates. The rules seem to be
    # 1. Balenced polymorphism then everything stays the same and
    #    END = START + len(REF) - 1
    # 2. Simple Insertion - START = POS + 1; END = POS: so END is > START, this
    #    behaviour can be changed with the dbcoords=False flag
    # 3. Simple Deletion - START = POS+1; END = START + len(REF) -1
    # 4. Complex Insertion - START = POS + 1; END = POS, the same as (2)
    # 5. Complex Deletion - Not observed yet
    # 6. Unbalenced alleles with different first base - Not observed yet so
    #    should stay the same but are errored out above
    START = pos
    END = pos

    # INSERTION i.e. the shortest of the unbaleced alleles is the first
    # index (ref)
    if min_smallest == 0:
        # If we have a simple insertion, i.e. the lead allele is a -
        if new_alleles[0] == blank_allele:
            # if dbcords is True then START > END otherwise START == END
            # START = pos + dbcoords
            # END = pos
            START = pos + 1
            END = pos + (1 * (not dbcoords))
        else:
            # DELETION/INSERTION, i.e. the REF is a longer sequence string.
            # i.e. a base is removed from the REF and a longer sequence is
            # inserted in
            # TODO: Need to check this in a GVF format
            START = pos + 1
            END = START + len(new_alleles[0]) - 1
    else:
        # DELETION
        START = pos + 1
        END = START + len(new_alleles[0]) - 1

    return START, END, new_alleles[0], new_alleles[1:]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_exons(rc, ensembl_id):
    """Get all the exons and UTR regions for an ensembl gene ID.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    ensembl_gene_id : `str`
        An ensembl gene identifier to get the exons for

    Returns
    -------
    exons : `dict`
        Gene information including exons and UTRs.
    """
    return rc.get_lookup_id(ensembl_id, expand=True, utr=True)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_genes_within(rc, species, chr_name, start_pos, end_pos=None,
                     use_center=False, ref_point_start=None,
                     ref_point_end=None, biotypes=None, use_trans=False):
    """Get the genes that overlap a region +/- n optional flank on top.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`, optional, default: `NoneType`
        The end position of the region of interest. If it is not defined then
        ``end_pos`` = ``start_pos``.
    use_center : `bool`, optional, default: `False`
        Use the center position of the region of interest to define what is
        nearest. If the center point is not an integer it is cast into an
        integer - this will result in rounding down.
    ref_point_start : `int` or `NoneType`, optional, default: `NoneType`
        A reference start point within the selected regions to determine what
        the nearest gene is, if not given then ``start_pos`` is used unless
        ``use_center`` is ``True`` in which case the center point of the region
        is used.
    ref_point_end : `int` or `NoneType`, optional, default: `NoneType`
        A reference end point within the selected regions to determine what the
        nearest gene is, if not given then ``end_pos`` is used unless
        ``use_center`` is ``True`` in which case the center point of the region
        is used.
    biotypes : `NoneType` or `list` of `str`, optional, default: `NoneType`
        Restrict the assessment of the nearest gene to genes with these
        biotypes. ``NoneType`` means no restriction on biotype.
    use_trans : `bool`, optional, default: `False`
        Use the coordinates of the cannonical transcript for each gene, rather
        than the gene coordinates.

    Returns
    -------
    overlap_genes : `dict` or `NoneType`
        Information on the nearest gene (or canonical transcript if
        ``use_trans=True``).

    Notes
    -----
    By default the nearest gene is assess as the gene that is nearest either
    the start_pos or end_pos of the region of interest. The center position can
    be used instead if ``use_center`` is ``True``. Strand is not considered
    when determining the nearest gene. The algorithm works as follows:

    *. Query the length of the chromosome and make sure the region is contained
       within the bounds.
    *. Do any genes already overlap the region, if so then they are all
       returned.
    *. If not, then a region 100kbp upstream and downstream from the region of
       interest is queried and any nearest genes determined.
    *. If none are present regions regions are queried until a nearest gene is
       found or at the ends of the chromosome.
    """
    no_region = (None, None)

    query_func = _query_gene_region
    if use_trans is True:
        query_func = _query_trans_region

    query_window = 100000

    # Process the arguments, this makes sure they are valid and will adjust
    # the start/end if we want to use the center point as a reference
    chr_length, center_point, start_pos, end_pos = _process_args(
        rc, species, chr_name, start_pos, end_pos, use_center, biotypes
    )

    # If the user has not supplied a specific reference point to evaluate
    # against then we use the start_pos/end_pos which could be the center point
    # is use_center=True
    if ref_point_start is None:
        ref_point_start = start_pos
    if ref_point_end is None:
        ref_point_end = end_pos

    seen_genes = set()
    genes = []
    passed_biotype = []
    for left_region, right_region in yield_chr_coords(
            rc, species, chr_name, center_point, center_point,
            batch_length=query_window, left_bounds=start_pos,
            right_bounds=end_pos
    ):
        if left_region != no_region:
            start, end = left_region
            genes.extend(
                query_func(rc, species, chr_name, start, end,
                           seen=seen_genes)
            )
        if right_region != no_region:
            start, end = right_region
            genes.extend(
                query_func(rc, species, chr_name, start, end,
                           seen=seen_genes)
            )

    # If no genes have been found
    if len(genes) == 0:
        return genes

    if biotypes is not None:
        passed_biotype = \
            [i for i in genes if i['biotype'] in biotypes]
    else:
        passed_biotype = genes

    requested_region_length = end_pos - start_pos + 1

    # Calculate the min distance and distance rank according to the reference
    # point
    get_min_dist(chr_name, ref_point_start, ref_point_end, genes)
    for idx, i in enumerate(passed_biotype, 1):
        i['distance_rank_biotype'] = idx
        i['requested_region_length'] = requested_region_length
    return passed_biotype


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_n_nearest_genes(rc, species, chr_name, start_pos, end_pos=None,
                        use_center=False, biotypes=None, ngenes=1,
                        use_trans=False):
    """Get the N nearest genes to the input coords.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`, optional, default: `NoneType`
        The end position of the region of interest. If it is not defined then
        ``end_pos`` = ``start_pos``.
    use_center : `bool`, optional, default: `False`
        Use the center position of the region of interest to define what is
        nearest. If the center point is not an integer it is cast into an
        integer - this will result in rounding down.
    biotypes : `NoneType` or `list` of `str`, optional, default: `NoneType`
        Restrict the assessment of the nearest gene to genes with these
        biotypes. ``NoneType`` means no restriction on biotype.
    ngenes : `int`, optional, default: `1`
        The number of nearest genes to return.
    use_trans : `bool`, optional, default: `False`
        Use the coordinates of the cannonical transcript for each gene, rather
        than the gene coordinates.

    Returns
    -------
    nearest_genes : `dict`
        Information on the nearest gene (or canonical transcript if
        ``use_trans=True``).

    Notes
    -----
    By default the nearest gene is assess as the gene that is nearest either
    the start_pos or end_pos of the region of interest. The center position can
    be used instead if ``use_center`` is ``True``. Strand is not considered
    when determining the nearest gene. The algorithm works as follows:

    *. Query the length of the chromosome and make sure the region is contained
       within the bounds.
    *. Do any genes already overlap the region, if so then they are all
       returned.
    *. If not, then a region 100kbp upstream and downstream from the region of
       interest is queried and any nearest genes determined.
    *. If none are present regions regions are queried until a nearest gene is
       found or at the ends of the chromosome.
    """
    no_region = (None, None)
    query_window = 100000

    query_func = _query_gene_region
    if use_trans is True:
        query_func = _query_trans_region

    # Process the arguments, this makes sure they are valid and will adjust
    # the start/end if we want to use the center point as a reference
    chr_length, center_point, start_pos, end_pos = _process_args(
        rc, species, chr_name, start_pos, end_pos, use_center, biotypes
    )

    seen_genes = set()
    genes = []
    passed_biotype = []
    for left_region, right_region in yield_chr_coords(
            rc, species, chr_name, center_point, center_point,
            batch_length=query_window
    ):
        # print(left_region, right_region)
        if left_region != no_region:
            start, end = left_region
            genes.extend(
                query_func(rc, species, chr_name, start, end,
                           seen=seen_genes)
            )
        if right_region != no_region:
            start, end = right_region
            genes.extend(
                query_func(rc, species, chr_name, start, end,
                           seen=seen_genes)
            )
        if len(genes) >= ngenes:
            if biotypes is not None:
                passed_biotype = \
                    [i for i in genes if i['biotype'] in biotypes]
            else:
                passed_biotype = genes
            if len(passed_biotype) >= ngenes:
                break

    get_min_dist(chr_name, start_pos, end_pos, genes)
    for idx, i in enumerate(passed_biotype, 1):
        i['distance_rank_biotype'] = idx
    return passed_biotype[:ngenes]


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def yield_chr_coords(rc, species, chr_name, start_pos, end_pos,
                     batch_length=100000, left_bounds=1, right_bounds=None):
    """Yield batches for leftwards and rightwards coordinates from a region on
    a chromosome until we reach the extent of the chromosome or the
    ``left_bounds``/``right_bounds``.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`, optional, default: `NoneType`
        The end position of the region of interest. If it is not defined then
        ``end_pos`` = ``start_pos``.
    batch_length : `int`, optional, default: `100000`
        The step range for each yielded left/right coordinates
    left_bounds : `int`, optional, default: `1`
        The lower boundary, when down to this the left coordinates will yield
        (None, None) (assuming the right co-ordinates are still in range).
    right_bounds : `int` or `NoneType`, optional, default: `NoneType`
        The upper boundary, when reached the right coordinates will yield
        (None, None) (assuming the left co-ordinates are still in range).
        ``NoneType`` means the end position of the chromosome.

    Yields
    ------
    left_region : `tuple` of (`int`, `int`) or (`NoneType`, `NoneType`)
        The left region in ``batch_length`` increments. When the left end
        (``left_bounds``) is reached this will be (`NoneType`, `NoneType`).
    right_region : `tuple` of (`int`, `int`) or (`NoneType`, `NoneType`)
        The right region in ``batch_length`` increments. When the right end
        (``right_bounds``) is reached this will be (`NoneType`, `NoneType`).

    Notes
    -----
    This will keep on yielding defined regions of batch_length until
    ``left_bounds`` and ``right_bounds`` is reached. If one or the other
    boundary is reached first we carry on going until the other boundary is
    reached but yield (None, None) for the out of range one.
    """
    try:
        # There is a module level cache of chromosome data to prevent excessive
        # rest queries, so we try to use that first.
        chr_info = _CHROMOSOME_CACHE[species][chr_name]
    except KeyError:
        # Not in the cache, so we query and update the cache
        _CHROMOSOME_CACHE.setdefault(species, {})
        chr_info = rc.get_info_region_assembly(chr_name, species)
        _CHROMOSOME_CACHE[species][chr_name] = chr_info

    # Extract the chromosome length
    chr_length = chr_info['length']

    # NoneType means that we are defaulting tot he max chromosome coordinates
    if right_bounds is None:
        right_bounds = chr_length

    # Ensure any user defined left boundary is within a meaningful range
    left_bounds = max(1, left_bounds)

    # If the user has set the right bounds then make sure it is in range of the
    # chromosome coordinates
    right_bounds = min(right_bounds, chr_length)

    # The left/right coordinate positions that will be updated with each
    # iteration
    left_start = start_pos
    left_end = None
    right_start = None
    right_end = end_pos

    # Booleans that indicate if we want to yield left/right coordinates. When
    # both are False we will exit. If one or the other is False we carry on
    # going but yield (None, None) for the out of range one
    query_left = True
    query_right = True
    while query_left | query_right:
        # The default is out of range
        left_region = (None, None)
        right_region = (None, None)

        # If leftwards is not out of range
        if query_left is True:
            left_end = left_start
            left_start = left_end - batch_length

            # The start position of the left boundary is out of range so we
            # adjust to the limits and indicate that we do not want any more
            # left regions yielded
            if left_start <= left_bounds:
                left_start = left_bounds
                query_left = False
            left_region = (left_start, left_end)

        # If rightwards is not out of range
        if query_right is True:
            right_start = right_end
            right_end = right_start + batch_length

            if right_end >= right_bounds:
                right_end = right_bounds
                query_right = False
            right_region = (right_start, right_end)
        yield left_region, right_region


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def valid_biotypes(rc, species, test_biotypes, object_type=None,
                   use_cache=True):
    """Test to see if biotypes are known valid Ensembl biotypes.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    test_biotypes : `list` of `str`
        A list of biotypes to test for validity.
    object_type : `str` or `NoneType`, optional, default: `NoneType`
        The object type for the biotype, so some biotypes are only appropriate
        for genes and others transcripts. Leaving this as ``NoneType`` will
        validate and ignore any object types.
    use_cache : `bool`, optional, default: `True`
        Use the module level cache.

    Raises
    ------
    KeyError
        If any of ``test_biotypes`` are not valid biotypes.

    Notes
    -----
    There is a module level cache of biotypes associated with a species to
    prevent repeated queries to Ensembl. Please note, this is not that
    sophisticated and has no concept of different rest clients. So will not
    known that difference between GRCh37 and GRCh38 biotypes (if there are
    any!?). If this is an issue set ``use_cache=False``.
    """
    try:
        # Will keep a module level cache of the biotypes for the species
        # so we do not have to continually query
        valid_biotypes = _BIOTYPE_CACHE[species]
    except KeyError:
        valid_biotypes = rc.get_info_biotypes(species)

        if use_cache is True:
            _BIOTYPE_CACHE[species] = valid_biotypes

    if object_type is not None:
        valid_biotypes = [
            i['biotype'] for i in valid_biotypes if object_type in i['objects']
        ]
    else:
        valid_biotypes = [
            i['biotype'] for i in valid_biotypes
        ]

    failed_biotypes = [i for i in test_biotypes if i not in valid_biotypes]

    if len(failed_biotypes) > 0:
        raise KeyError(
            "biotypes not valid: '{0}".format(','.join(failed_biotypes))
        )


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_min_dist(chr_name, start_pos, end_pos, regions):
    """Calculate the minimum distance between each region and the region
    represented by ``chr_name``, ``start_pos``, ``end_pos``.

    Parameters
    ----------
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`
        The end position of the region of interest.
    regions : `list` of `dict`
        Regions that have been queried via a REST query. Each dict must have
        the keys ``seq_region_name`` (``str``), ``start`` (``int``), ``end``
        (``int``).

    Returns
    -------
    regions : `list` of `dict`
        The regions that were passed to this function but with additional keys
        added. ``abs_min_dist`` (``int``), ``min_dist`` (``int``), ``overlap``
        (``int``), ``distance_rank`` (``int``) and ``region_length`` (``int``).
        All additions are in place.

    Raises
    ------
    ValueError
        If the length of the regions is 0.
    """
    min_start = None
    max_end = None

    if len(regions) == 0:
        raise ValueError("there are no regions to evaluate for min distance")

    for i in regions:
        i['abs_min_dist'] = 5E09
        i['min_dist'] = 5E09
        i['overlap'] = 0

        if i['seq_region_name'] == chr_name:
            # First check for an overlap
            i['overlap'] = get_overlap(
                (chr_name, start_pos, end_pos),
                (i['seq_region_name'], i['start'], i['end'])
            )

            try:
                min_start = min(min_start, i['start'])
                max_end = max(max_end, i['end'])
            except TypeError:
                min_start = i['start']
                max_end = i['end']

            if i['overlap'] > 0:
                i['abs_min_dist'] = -1
                i['min_dist'] = -1
                continue

            distances = sorted(
                [
                    (abs(i), i) for i in [i['start'] - start_pos,
                                          i['end'] - start_pos,
                                          i['start'] - end_pos,
                                          i['end'] - end_pos]
                ],
                key=lambda x: x[0]
            )
            i['abs_min_dist'] = distances[0][0]
            i['min_dist'] = distances[0][1]

    regions.sort(key=lambda x: x['overlap'], reverse=True)
    regions.sort(key=lambda x: x['abs_min_dist'])

    region_length = max_end - min_start + 1
    for idx, i in enumerate(regions, 1):
        i['distance_rank'] = idx
        i['region_length'] = region_length
    return regions


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_region_definition(chr_name, start_pos, end_pos):
    """Generate a text chr/pos scan for use in a rest query.

    Parameters
    ----------
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`
        The end position of the region of interest.

    Returns
    -------
    chr_pos : `str`
        Will have the format ``<chr_name>:<start_pos>-<end_pos>``.

    Notes
    -----
    This does not do any sanity checks.
    """
    return '{0}:{1}-{2}'.format(chr_name, start_pos, end_pos)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def get_overlap(x, y):
    """Find the overlap between two regions. The start and end coordinates are
    cast to int before comparing.

    Parameters
    ----------
    x : `tuple` (`int`, `int`)
        The min/max of the first feature.
    y : `tuple` (`int`, `int`)
        The min/max of the second feature.

    Returns
    -------
    overlap :`int`
        The overlap between the `x` site and the `y` site or 0 if there is no
        overlap
    """
    if x[0] != y[0]:
        return 0

    # Make sure we have all ints
    # start_x, end_x, start_y, end_y = int(start_x), int(end_x), int(start_y),\
    #      int(end_y)

    # start_y -= 1
    # start_x -= 1

    # The +1 is to get <= or >=
    return max(0, min(x[2], y[2]) - max(x[1], y[1]))


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _process_args(rc, species, chr_name, start_pos, end_pos, use_center,
                  biotypes):
    """Validate arguments given to ``get_genes_within`` and
    ``get_n_nearest_genes``.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`
        The end position of the region of interest. If it is not defined then
        ``end_pos`` = ``start_pos``.
    use_center : `bool`
        Use the center position of the region of interest to define what is
        nearest. If the center point is not an integer it is cast into an
        integer - this will result in rounding down.
    biotypes : `NoneType` or `list` of `str`
        Restrict the assessment of the nearest gene to genes with these
        biotypes. ``NoneType`` means no restriction on biotype.
    """
    end_pos = end_pos or start_pos

    if end_pos < start_pos:
        raise IndexError("end position < start position: {0} < {1}")

    if biotypes is not None:
        # Make sure the biotypes are valid otherwise we could query loads
        valid_biotypes(rc, species, biotypes, object_type='gene')

    # The center point will always be rounded down if it is not an integer
    center_point = int(start_pos + ((end_pos - start_pos)/2))

    if use_center is True:
        start_pos = center_point
        end_pos = start_pos

    try:
        chr_info = _CHROMOSOME_CACHE[species][chr_name]
    except KeyError:
        _CHROMOSOME_CACHE.setdefault(species, {})
        chr_info = rc.get_info_region_assembly(chr_name, species)
        _CHROMOSOME_CACHE[species][chr_name] = chr_info
    chr_length = chr_info['length']

    if start_pos < 1 or end_pos > chr_length:
        raise IndexError(
            "region coordinates out of bounds: chr{0} = {1} start={2},"
            " end={3}".format(chr_name, chr_length, start_pos, end_pos)
        )

    return chr_length, center_point, start_pos, end_pos


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _query_gene_region(rc, species, chr_name, start_pos, end_pos, seen=None):
    """Query for genes overlapping a region.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`
        The end position of the region of interest
    seen : `set` or `NoneType`, optional, default: `NoneType`
        A set of gene IDs (`str`) that have already been encountered so
        will not be stored and returned.

    Returns
    -------
    all_features : `dict`
        All the genes that overlap the region.
    """
    if seen is None:
        seen = set([])
    feature = 'gene'
    all_features = []

    region_def = get_region_definition(chr_name, start_pos, end_pos)
    for feature in rc.get_overlap_region(feature, region_def, species):
        if feature['id'] not in seen:
            seen.add(feature['id'])
            all_features.append(feature)
    return all_features


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _query_trans_region(rc, species, chr_name, start_pos, end_pos, seen=None):
    """Query for canonical transcripts overlapping a region.

    Parameters
    ----------
    rc : `ensembl_rest_client.client.Rest`
        A rest client that implements the ``info`` and ``overlap`` interfaces.
    species : `str`
        An Ensembl species name, this is used to get the upper limit of the
        chromosome coordinates.
    chr_name : `str`
        The chromosome that contains the region of interest.
    start_pos : `int`
        The start position of the region of interest.
    end_pos : `int` or `NoneType`
        The end position of the region of interest.
    seen : `set` or `NoneType`, optional, default: `NoneType`
        A set of transcript IDs (`str`) that have already been encountered so
        will not be stored and returned.

    Returns
    -------
    all_features : `dict`
        All the canonical transcripts that overlap the region. Their parent
        gene data is also queried out in a separate query and added as a
        ``dict`` to a ``gene_data`` key.
    """
    if seen is None:
        seen = set([])
    feature = 'transcript'
    all_features = []

    region_def = get_region_definition(chr_name, start_pos, end_pos)
    for feature in rc.get_overlap_region(feature, region_def, species):
        if feature['is_canonical'] == 1 and feature['id'] not in seen:
            seen.add(feature['id'])
            all_features.append(feature)

    # Now query out the gene data for each of the canonical transcripts
    for feature in all_features:
        try:
            feature['gene_data'] = rc.get_lookup_id(
                feature['Parent'], db_type='core', expand=None,
                data_format=None, phenotypes=None, species=species
            )
        except KeyError:
            feature['gene_data'] = dict(
                id=feature['id'], display_name=feature['external_name']
            )

    return all_features
