import os
from greent.util import Resource
from greent.graph_components import LabeledID
from collections import defaultdict
import itertools
from collections import OrderedDict

#TODO: should all of this be done with some sort of canned semantic tools?
class Concept:
    """ A semantic type or concept. A high level idea comprising one or more identifier namespace.
    Provides rudimentary notion of specialization via is_a. """
    def __init__(self, name, is_a, id_prefixes):
        self.name = name
        #Only a single parent?
        self.is_a = is_a
        is_a_name = is_a.name if is_a else None
        self.id_prefixes = [] if id_prefixes is None else id_prefixes
    def __repr__(self):
        return f"Concept(name={self.name},is_a={self.is_a is not None},id_prefixes={self.id_prefixes})"
        #return f"Concept(name={self.name},is_a={self.is_a},id_prefixes={self.id_prefixes})"

class Relationship:
    """ A semantic type for a relationship (or slot)
    Provides rudimentary notion of specialization via is_a. """
    def __init__(self, name, is_a, mappings):
        self.name = name
        #Only a single parent?
        self.is_a = is_a
        is_a_name = is_a.name if is_a else None
        self.mappings = [] if mappings is None else mappings
        self.identifier = self.mint_identifier()
    def mint_identifier(self):
        """Find out what we are going to use as an identifier.
        If we don't have any mappings, mint one based on the name.
        Otherwise, prefer a set of curies from the mappings.  Failing anything else,
        use the first mapping."""
        if len(self.mappings) == 0:
            return f'BIOLINK:{self.name}'
        favorites=['BIOLINK','BFO','RO','SIO','GENO','SEMMEDDB']
        for favorite in favorites:
            for mapped in self.mappings:
                if mapped.startswith(favorite):
                    return mapped
        return self.mappings[0]
    def __repr__(self):
        return f"Relation(name={self.name},is_a={self.is_a is not None},mappings={self.mappings})"
        #return f"Concept(name={self.name},is_a={self.is_a},id_prefixes={self.id_prefixes})"


class ConceptModel:
    """ A grouping of concepts.
    Should ultimately be generalizable to different concept models. We begin with the biolink-model. """
    
    def __init__(self, name):
        self.name = name
        self.by_name = {} #defaultdict(lambda:None)
        self.by_prefix = defaultdict(lambda:None)
        self.relations_by_name = defaultdict(lambda:None)
        self.relations_by_xref = defaultdict(lambda:None)

        self.model_loaders = {
            'biolink-model' : lambda : BiolinkConceptModelLoader (name, self)
        }
        if self.name in self.model_loaders:
            self.model_loaders[self.name] ()
        else:
            raise ValueError (f"A concept model loader for concept model {self.name} must be defined.")

        #Identifiers are specified at different points in the yaml.  We need to make sure that the set of identifiers
        # gets propagated to all the nodes.  Because every subclass is also a superclass, the identifiers used for
        # a subtype have to also be usable for the supertype. For instance, CL (for cell) must also be allowed
        # for an anatomical entity, because every cell is an anatomical entity.  For convenience, we also want to
        # allow for the case when we specify something like chemical_substance, and have its ids pushed down to
        # drug and metabolite in a consistent way.  If we push up and then down, we'll end up with everything can be
        # anything.  So we need to push down and then up.  If we push down, we should only push down into
        # empty concepts (i.e. those that do not already have an id_mapping.
        self.create_id_prefixes()

        """ Design discussions are ongoing about how best to model concepts with respect to ids. Manual annotation
        provides the most reliable and granular approach but is time intensive. It's unclear when this standard might
        be met. Another approach is to reason about concepts based on identifiers. This overloads the semantics of 
        identifiers which is concerning. For now, we plan to look first in the model. If a curator has made an assertion
        about an association between the model and an identifier, we prioritize that. If not, we guess based on a map
        of associations between identifiers and concepts. """
        identifier_map_path = os.path.join (os.path.dirname (__file__), "conf", "identifier_map.yaml")
        self.the_map = Resource.load_yaml (identifier_map_path)['identifier_map']
        #for c in self.by_name.values ():
        #    print (f"by name {c}")

    def create_id_prefixes(self):
        top_set = self.get_roots()
        while len(top_set) > 0:
            next = top_set.pop()
            children = [self.get(c) for c in self.get_children(next.name)]
            top_set.extend(children)
            if len(next.id_prefixes) > 0:
                for child in children:
                    if len(child.id_prefixes) == 0:
                        child.id_prefixes.extend(next.id_prefixes)
        roots = self.get_roots()
        for root in roots:
            self.recursive_combine_id_maps(root)

    def recursive_combine_id_maps(self,concept):
        #First, get the combined children identifiers
        children = [self.get(c) for c in self.get_children(concept.name)]
        child_prefixes = ( self.recursive_combine_id_maps(child) for child in children )
        #interleave the children.  We don't have a smarter way ATM
        combined_prefixes=[x for x in itertools.chain(*itertools.zip_longest(*child_prefixes)) if x is not None]
        #Add new items to the end of the current id_prefixes
        concept.id_prefixes.extend(combined_prefixes)
        #This can leave us with some repeat identifiers, uniqueify, but preserve order:
        concept.id_prefixes = list(OrderedDict.fromkeys(concept.id_prefixes))
        return concept.id_prefixes

    def get (self, concept_name):
        return self.by_name[concept_name]
    
    def add_item (self, concept):
        self.by_name [concept.name] = concept
        for prefix in concept.id_prefixes:
            self.by_prefix[prefix] = concept

    def add_relationship(self,relationship):
        self.relations_by_name[relationship.name] = relationship
        for mapping in set(relationship.mappings):
            if mapping in self.relations_by_xref:
                raise Exception('Have multiple slots with the same mapping {}'.format(mapping))
            self.relations_by_xref[mapping] = relationship

    def items (self):
        return  self.by_name.items ()

    def get_concepts_by_prefix (self, id_list):
        return [ self.by_prefix[k].name for k in id_list if k in self.by_prefix ]

    def get_single_concept_by_prefixes (self, id_list):
        possible_concepts = defaultdict(lambda: [None, 0])
        for k in id_list:
            if k in self.by_prefix:
                node = possible_concepts[self.by_prefix[k]]
                node[0] = k
                node[1] = node[1] + 1
        ordered = sorted(possible_concepts.items (), key=lambda item: item[1])
        return ordered[0][0] if len(ordered) > 0 else None

    def get_leaves(self):
        """Return all concepts that are not parents of another concept"""
        leaves = set( self.by_name.values() )
        for concept_name, concept in self.by_name.items():
            if concept.is_a is not None:
                leaves.discard(concept.is_a)
        return leaves

    def get_roots(self):
        """Return all concepts that are parents but do not have parents"""
        parents = set( [concept.is_a for name,concept in self.by_name.items()] )
        return list(filter( lambda x: x is not None and x.is_a is None, parents))

    #Note that this assumes a tree.  OK for biolink model, but not if we append e.g. a gene_product is_a chemical_substance
    def get_parent(self,concept_name):
        """Return the parent of a concept"""
        return self.get(concept_name).is_a.name

    def get_children(self,concept_name):
        """Return the children of a concept"""
        children_name_concept = list( filter(lambda item: item[1].is_a is not None and item[1].is_a.name == concept_name, self.by_name.items() ))
        children = [x[0] for x in children_name_concept]
        return children

    def standardize_relationship(self,relationship):
        xref = relationship.identifier
        r = self.relations_by_xref[xref]
        if r is None:
            return LabeledID(identifier="GAMMA:0", label="Unmapped_Relation")
        else:
            return LabeledID(identifier=r.identifier, label=r.name)

class ConceptModelLoader:

    def __init__(self, name, concept_model):
        self.name = name
        self.model = concept_model
        model_path = os.path.join (os.path.dirname (__file__), "conf", f"{self.name}.yaml")
        model_obj = Resource.load_yaml (model_path)

        model_overlay_path = model_path.replace (".yaml", "_overlay.yaml")
        if os.path.exists (model_overlay_path):
            model_overlay = Resource.load_yaml (model_overlay_path)
            #Update only adds/overwrites keys at the top level. Here, it just overwrites "Classes" rather than updating classes.
            #model_obj.update (model_overlay)
            #This version recursively updates throughout the hierarchy of dicts, updating lists also
            # if we specify id_prefixes in the overlay, ignore what's in the original yaml.
            Resource.deepupdate(model_obj, model_overlay, overwrite_keys = ['id_prefixes'])
        for name,obj in model_obj["classes"].items():
            concept = self.parse_item (name,obj)
            self.model.add_item (concept)

        for name,obj in model_obj['slots'].items():
            relationship = self.parse_slot(name,obj)
            self.model.add_relationship(relationship)

    def parse_item (self, obj):
        raise ValueError ("Not implemented")

    def parse_slot (self, obj):
        raise ValueError ("Not implemented")


class BiolinkConceptModelLoader (ConceptModelLoader):
    def __init__(self, name, concept_model):
        super(BiolinkConceptModelLoader, self).__init__(name, concept_model)
                
    def parse_item (self, originalname, obj):
        name = originalname.replace (" ", "_")
        is_a = obj["is_a"].replace (" ", "_") if "is_a" in obj else None
        id_prefixes = obj["id_prefixes"] if "id_prefixes" in obj else []
        parent = self.model.by_name [is_a] if is_a in self.model.by_name else None
        return Concept (name = name, is_a = parent, id_prefixes = id_prefixes)

    def parse_slot(self,originalname,obj):
        name = originalname.replace (" ", "_")
        mappings = obj["mappings"] if "mappings" in obj else []
        is_a = obj["is_a"].replace (" ", "_") if "is_a" in obj else None
        parent = self.model.relations_by_name [is_a] if is_a in self.model.relations_by_name else None
        return Relationship (name = name, is_a = parent, mappings = mappings)

