"""
==============================================
KEGG - Kyoto Encyclopedia of Genes and Genomes
==============================================

:mod:`kegg` is a python module for accessing `KEGG (Kyoto Encyclopedia
of Genes and Genomes) <http://www.genome.jp/kegg/>`_ using its web services.

.. note:: This module requires `slumber`_ and `requests`_ packages.

.. _`slumber`: https://pypi.python.org/pypi/slumber/

.. _`requests`: https://pypi.python.org/pypi/requests


>>> from Orange.bio.kegg import *
>>> # Create a KEGG Genes database interface
>>> genome = KEGGGenome()
>>> # List all available entry ids
>>> keys = genome.keys()
>>> print keys[0]
T01001
>>> # Retrieve the entry for the key.
>>> entry = genome[keys[0]]
>>> print entry.entry_key
T01001
>>> print entry.definition
Homo sapiens (human)
>>> print str(entry)
ENTRY       T01001            Complete  Genome
NAME        hsa, HUMAN, 9606
DEFINITION  Homo sapiens (human)
...

The :class:`Organism` class can be a convenient starting point
for organism specific databases.

>>> organism = Organism("Homo sapiens")  # searches for the organism by name
>>> print organism.org_code  # prints the KEGG organism code
hsa
>>> genes = organism.genes  # get the genes database for the organism
>>> gene_ids = genes.keys() # KEGG gene identifiers
>>> entry = genes["hsa:672"]
>>> print entry.definition
breast cancer 1, early onset
>>> print entry  # print the entry in DBGET database format.
ENTRY       672               CDS       T01001
NAME        BRCA1, BRCAI, BRCC1, BROVCA1, IRIS, PNCA4, PPP1R53, PSCP, RNF53
DEFINITION  breast cancer 1, early onset
...

"""
from __future__ import absolute_import

import os
import sys
import urllib2
import threading

from collections import defaultdict
from itertools import chain
from datetime import datetime
from contextlib import contextmanager

from Orange.utils import lru_cache
from Orange.utils import progress_bar_milestones
from Orange.utils import (
    deprecated_keywords, deprecated_attribute, deprecated_function_name
)

from .. import utils, taxonomy as obiTaxonomy
obiProb = utils.stats

from . import databases
from . import entry

from .brite import BriteEntry, Brite

from . import api
from . import conf
from . import pathway

KEGGGenome = databases.Genome
KEGGGenes = databases.Genes
KEGGEnzyme = databases.Enzyme
KEGGReaction = databases.Reaction
KEGGPathways = databases.Pathway
KEGGCompound = databases.Compound

KEGGBrite = Brite
KEGGBriteEntry = BriteEntry

KEGGPathway = pathway.Pathway

DEFAULT_CACHE_DIR = conf.params["cache.path"]


class Organism(object):
    """
    A convenience class for retrieving information regarding an
    organism in the KEGG Genes database.

    :param str org: KEGG organism code (e.g. "hsa", "sce"). Can also be a
        descriptive name (e.g. 'yeast', "homo sapiens") in which case the
        organism code will be searched for by using KEGG `find` api.

    .. seealso::

        :func:`organism_name_search`
            Search KEGG for an organism code

    """
    def __init__(self, org, genematcher=None):
        self.org_code = self.organism_name_search(org)
        self.genematcher = genematcher
        self.api = api.CachedKeggApi()

    @property
    def org(self):
        """
        KEGG organism code.
        """
        return self.org_code

    @property
    def genes(self):
        """
        An :class:`~.databases.Genes` database instance for this organism.
        """
        # TODO: This should not be a property but a method.
        # I think it was only put here as back compatibility with old obiKEGG.
        if not hasattr(self, "_genes"):
            genes = KEGGGenes(self.org_code)
            self._genes = genes
        return self._genes

    def gene_aliases(self):
        """
        Return a list of sets of equal genes (synonyms) in KEGG for
        this organism.

        .. note::

            This only includes 'ncbi-geneid' and 'ncbi-gi' records
            from the KEGG Genes DBLINKS entries.

        """
        definitions = self.api.list(self.org_code)
        ncbi_geneid = self.api.conv(self.org_code, "ncbi-geneid")
        ncbi_gi = self.api.conv(self.org_code, "ncbi-gi")

        aliases = defaultdict(set)

        for entry_id, definition in definitions:
            # genes entry id without the organism code
            aliases[entry_id].add(entry_id.split(":", 1)[1])
            # all names in the NAME field (KEGG API list returns
            # 'NAME; DEFINITION') fields for genes
            names = definition.split(";")[0].split(",")
            aliases[entry_id].update([name.strip() for name in names])

        for source_id, target_id in chain(ncbi_geneid, ncbi_gi):
            aliases[target_id].add(source_id.split(":", 1)[1])

        return [set([entry_id]).union(names)
                for entry_id, names in aliases.iteritems()]

    def pathways(self, with_ids=None):
        """
        Return a list of all pathways for this organism.
        """
        if with_ids is not None:
            return self.api.get_pathways_by_genes(with_ids)
        else:
            return [p.entry_id for p in self.api.list_pathways(self.org_code)]

    def list_pathways(self):
        """
        List all pathways for this organism.

        .. deprecated: 2.5
            Use :func:`pathways` instead.

        """
        # NOTE: remove/deprecate and use pathways()
        return self.pathways()

    def get_linked_pathways(self, pathway_id):
        self.api.get_linked_pathways(pathway_id)

    def enzymes(self, genes=None):
        raise NotImplementedError()

    def get_enriched_pathways(self, genes, reference=None,
                              prob=obiProb.Binomial(), callback=None):
        """
        Return a dictionary with enriched pathways ids as keys
        and (list_of_genes, p_value, num_of_reference_genes) tuples
        as items.

        """
        if reference is None:
            reference = self.genes.keys()
        reference = set(reference)

        allPathways = defaultdict(lambda: [[], 1.0, []])
        milestones = progress_bar_milestones(len(genes), 100)
        pathways_db = KEGGPathways()

        pathways_for_gene = []
        for i, gene in enumerate(genes):
            pathways_for_gene.append(self.pathways([gene]))
            if callback and i in milestones:
                callback(i * 50.0 / len(genes))

        # pre-cache for speed
        pathways_db.pre_cache([pid for pfg in pathways_for_gene
                               for pid in pfg])
        for i, (gene, pathways) in enumerate(zip(genes, pathways_for_gene)):
            for pathway in pathways:
                if pathways_db.get_entry(pathway).gene:
                    allPathways[pathway][0].append(gene)
            if callback and i in milestones:
                callback(50.0 + i * 50.0 / len(genes))

        pItems = allPathways.items()

        for i, (p_id, entry) in enumerate(pItems):
            pathway = pathways_db.get_entry(p_id)
            entry[2].extend(reference.intersection(pathway.gene or []))
            entry[1] = prob.p_value(len(entry[0]), len(reference),
                                    len(entry[2]), len(genes))
        return dict([(pid, (genes, p, len(ref)))
                     for pid, (genes, p, ref) in allPathways.items()])

    def get_genes_by_enzyme(self, enzyme):
        enzyme = KEGGEnzyme().get_entry(enzyme)
        return enzyme.genes.get(self.org_code, []) if enzyme.genes else []

    def get_genes_by_pathway(self, pathway_id):
        return KEGGPathway(pathway_id).genes()

    def get_enzymes_by_pathway(self, pathway_id):
        return KEGGPathway(pathway_id).enzymes()

    def get_compounds_by_pathway(self, pathway_id):
        return KEGGPathway(pathway_id).compounds()

    def get_pathways_by_genes(self, gene_ids):
        """ Pathways that include all genes in gene_ids. """
        l = self.api.get_genes_pathway_organism(self.org_code)
        gene_ids = set(gene_ids)
        gtp = defaultdict(set)
        for a,b in l:
            gtp[a].add(b)
        pathways = [ gtp[g] for g in gene_ids ]
        pathways = reduce(set.intersection, pathways)
        return sorted(pathways)

    def get_pathways_by_enzymes(self, enzyme_ids):
        enzyme_ids = set(enzyme_ids)
        pathways = [KEGGEnzyme()[id].pathway for id in enzyme_ids]
        pathways = reduce(set.union, pathways, set())
        return [id for id in pathways
                if enzyme_ids.issubset(KEGGPathway(id).enzymes())]

    def get_pathways_by_compounds(self, compound_ids):
        compound_ids = set(compound_ids)
        pathways = [KEGGCompound()[id].pathway for id in compound_ids]
        pathways = reduce(set.union, pathways, set())
        return [id for id in pathways
                if compound_ids.issubset(KEGGPathway(id).compounds())]

    def get_enzymes_by_compound(self, compound_id):
        return KEGGCompound()[compound_id].enzyme

    def get_enzymes_by_gene(self, gene_id):
        return self.genes[gene_id].enzymes

    def get_compounds_by_enzyme(self, enzyme_id):
        return self._enzymes_to_compounds.get(enzyme_id)

    @deprecated_keywords({"caseSensitive": "case_sensitive"})
    def get_unique_gene_ids(self, genes, case_sensitive=True):
        """
        Return a tuple with three elements. The first is a dictionary
        mapping from unique geneids to gene names in genes, the second
        is a list of conflicting gene names and the third is a list of
        unknown genes.

        """
        unique, conflicting, unknown = {}, [], []
        for gene in genes:
            names = self.genematcher.match(gene)
            if len(names) == 1:
                unique[names[0]] = gene
            elif len(names) == 0:
                unknown.append(gene)
            else:
                conflicting.append(gene)
        return unique, conflicting, unknown

    @deprecated_function_name
    def get_genes(self):
        return self.genes

    @classmethod
    def organism_name_search(cls, name):
        return organism_name_search(name)

    @classmethod
    def organism_version(cls, name):
        name = cls.organism_name_search(name)
        with _global_genome_instance() as genome:
            info = genome.api.info(name)
            return info.release

    def _set_genematcher(self, genematcher):
        setattr(self, "_genematcher", genematcher)

    def _get_genematcher(self):
        if getattr(self, "_genematcher", None) is None:
            from .. import obiGene
            if self.org_code == "ddi":
                self._genematcher = obiGene.matcher(
                    [obiGene.GMKEGG(self.org_code), obiGene.GMDicty(),
                     [obiGene.GMKEGG(self.org_code), obiGene.GMDicty()]]
                )
            else:
                self._genematcher = obiGene.matcher(
                    [obiGene.GMKEGG(self.org_code)])

            self._genematcher.set_targets(self.genes.keys())
        return self._genematcher

    genematcher = property(_get_genematcher, _set_genematcher)


KEGGOrganism = Organism


def organism_name_search(name):
    """
    Search for a organism by `name` and return it's KEGG organism code.
    """
    with _global_genome_instance() as genome:
        try:
            name = genome.org_code_to_entry_key(name)
        except ValueError:
            pass

        if name not in genome:
            ids = genome.search(name)
            if ids:
                name = ids.pop(0) if ids else name
            else:
                raise obiTaxonomy.UnknownSpeciesIdentifier(name)

        try:
            return genome[name].organism_code
        except KeyError:
            raise obiTaxonomy.UnknownSpeciesIdentifier(name)


def pathways(org):
    """
    Return a list of all KEGG pathways for an KEGG organism code `org`.
    """
    return KEGGPathway.list(org)


def from_taxid(taxid):
    """
    Return a KEGG organism code for a an NCBI Taxonomy id string `taxid`.
    """
    with _global_genome_instance() as genome:
        res = genome.search(taxid)
        for r in res:
            e = genome[r]

            if e.taxid in [taxid, genome.TAXID_MAP.get(taxid, taxid)]:
                return e.org_code()

        return None


def to_taxid(name):
    """
    Return a NCBI Taxonomy id for a given KEGG Organism name
    """
    with _global_genome_instance() as genome:
        if name in genome:
            return genome[name].taxid

        keys = genome.search(name)
        if keys:
            return genome[keys[0]].taxid
        else:
            return None


def _global_genome_instance():
    genome = getattr(_global_genome_instance, "_genome", None)
    if genome is None:
        genome = KEGGGenome()
        genome._lock = threading.RLock()
        _global_genome_instance._genome = genome

    @contextmanager
    def instance_locked(instance, lock):
        lock.acquire()
        try:
            yield instance
        finally:
            lock.release()

    return instance_locked(genome, genome._lock)


def main():
    KEGGGenome()
    import doctest
    extraglobs = {"api": KeggApi()}
    doctest.testmod(optionflags=doctest.ELLIPSIS, extraglobs=extraglobs)


if __name__ == "__main__":
    sys.exit(main())
