

# Guiao de representacao do conhecimento
# -- Redes semanticas
# 
# Inteligencia Artificial & Sistemas Inteligentes I
# DETI / UA
#


# Classe Relation, com as seguintes classes derivadas:
#     - Association - uma associacao generica entre duas entidades
#     - Subtype     - uma relacao de subtipo entre dois tipos
#     - Member      - uma relacao de pertenca de uma instancia a um tipo
#

class Relation:
    def __init__(self,e1,rel,e2):
        self.entity1 = e1
        self.name = rel
        self.entity2 = e2
    def __str__(self):
        return self.name + "(" + str(self.entity1) + "," + \
               str(self.entity2) + ")"
    def __repr__(self):
        return str(self)


# Subclasse Association
class Association(Relation):
    def __init__(self,e1,assoc,e2):
        Relation.__init__(self,e1,assoc,e2)

#   Exemplo:
#   a = Association('socrates','professor','filosofia')

class AssocOne(Relation):
    def __init__(self, e1, rel, e2):
        Relation.__init__(self, e1, rel, e2)


class AssocNum(Relation):
    def __init__(self, e1, rel, e2):
        Relation.__init__(self, e1, rel, e2)


# Subclasse Subtype
class Subtype(Relation):
    def __init__(self,sub,super):
        Relation.__init__(self,sub,"subtype",super)


#   Exemplo:
#   s = Subtype('homem','mamifero')

# Subclasse Member
class Member(Relation):
    def __init__(self,obj,type):
        Relation.__init__(self,obj,"member",type)

#   Exemplo:
#   m = Member('socrates','homem')

# classe Declaration
# -- associa um utilizador a uma relacao por si inserida
#    na rede semantica
#
class Declaration:
    def __init__(self,user,rel):
        self.user = user
        self.relation = rel
    def __str__(self):
        return "decl("+str(self.user)+","+str(self.relation)+")"
    def __repr__(self):
        return str(self)

#   Exemplos:
#   da = Declaration('descartes',a)
#   ds = Declaration('darwin',s)
#   dm = Declaration('descartes',m)

# classe SemanticNetwork
# -- composta por um conjunto de declaracoes
#    armazenado na forma de uma lista
#
class SemanticNetwork:
    def __init__(self,ldecl=None):
        self.declarations = [] if ldecl==None else ldecl
    
    def __str__(self):
        return str(self.declarations)
    
    def insert(self,decl):
        self.declarations.append(decl)
    
    #informacao local (propriedades nao herdadas) sobre as varias entidades presentes na rede
    def query_local(self,user=None,e1=None,rel=None,e2=None):
        self.query_result = \
            [ d for d in self.declarations
                if  (user == None or d.user==user)
                and (e1 == None or d.relation.entity1 == e1)
                and (rel == None or d.relation.name == rel)
                and (e2 == None or d.relation.entity2 == e2) ]
        return self.query_result
    
    
    def show_query_result(self):
        for d in self.query_result:
            print(str(d))


    #Devolve lista dos nomes das associações (tipos de Association)
    def list_associations(self):
        return list(set(
            d.relation.name
            for d in self.declarations
            if isinstance(d.relation, Association)
        ))
    

    #Devolve a lista dos objectos declarados como (Member)
    def list_objects(self):
        return list(set(
            d.relation.entity1
            for d in self.declarations
            if isinstance(d.relation, Member)
        ))

    #Devolve lista de utilizadores que fizeram declarações na rede.
    def list_users(self):
        return list(set(
            d.user
            for d in self.declarations
        ))
    

    #Devolve lista dos tipos existentes (Member e Subtype).
    def list_types(self):
        types = set()

        for d in self.declarations:
            r = d.relation
            # Tipos usados em relações Member
            if isinstance(r, Member):
                types.add(r.entity2)
            # Tipos ligados por relações Subtype
            elif isinstance(r, Subtype):
                types.add(r.entity1)
                types.add(r.entity2)

        return list(types)
    

    #Dada uma entidade, devolve os nomes das suas associações
    def list_local_associations(self, entity):
        return list(set(
            d.relation.name
            for d in self.declarations
            if isinstance(d.relation, Association)
            and d.relation.entity1 == entity
        ))
    

    #Dado um user, devolve a lista dos nomes das relações por ele declaradas
    def list_relations_by_user(self, user):
        return list(set(
            d.relation.name
            for d in self.declarations
            if d.user == user
        ))
    
    #Número de associações diferentes declaradas por um utilizador
    def associations_by_user(self, user):
        assoc_names = set(
            d.relation.name
            for d in self.declarations
            if d.user == user and isinstance(d.relation, Association)
        )
        return len(assoc_names)
    

    #Dada uma entidade, devolve lista de tuplos (associação, user) para associações declaradas sobre essa entidade.
    def list_local_associations_by_entity(self, entity):
        return set(
            (d.relation.name, d.user)
            for d in self.declarations
            if isinstance(d.relation, Association)
            and d.relation.entity1 == entity
        )
    

    #Devolve True se A for predecessora (ascendente) de B (existe member/subtype B->A)
    def predecessor(self, A, B):
        parents_of_B = [
            d.relation.entity2
            for d in self.declarations
            if isinstance(d.relation, (Member, Subtype))
            and d.relation.entity1 == B
        ]

        return (
            A in parents_of_B
            or any(self.predecessor(A, p) for p in parents_of_B)
        )
    
    # def predecessor(self, A, B):
    #     # busca em profundidade (DFS)
    #     visited = set()
    #     stack = [B]   # começamos a subir a partir de B

    #     while stack:
    #         current = stack.pop()
    #         if current == A:
    #             return True

    #         if current in visited:
    #             continue
    #         visited.add(current)

    #         # encontrar todos os "pais" de current via Member/Subtype
    #         for d in self.declarations:
    #             r = d.relation
    #             # aresta current -> r.entity2 em Member ou Subtype
    #             if isinstance(r, (Member, Subtype)) and r.entity1 == current:
    #                 parent = r.entity2
    #                 if parent not in visited:
    #                     stack.append(parent)

    #     return False

    #Devolve uma lista [A, ..., B] se A for predecessora de B via relações Member/Subtype. Caso contrário, devolve None.
    def predecessor_path(self, A, B):
        predecessors_of_B = [
            d.relation.entity2
            for d in self.declarations
            if isinstance(d.relation, (Member, Subtype))
            and d.relation.entity1 == B
        ]

        if A in predecessors_of_B:
            return [A, B]

        for path in (self.predecessor_path(A, p) for p in predecessors_of_B):
            if path is not None:
                return path + [B]

        return None


    #Devolve todas as declaracoes de associacoes locais ou herdadas por uma entidade
    def query(self, entity, assoc_name=None):
        # associações herdadas dos predecessores (Member/Subtype)
        inherited_lists = [
            self.query(d.relation.entity2, assoc_name)
            for d in self.declarations
            if isinstance(d.relation, (Member, Subtype))
            and d.relation.entity1 == entity
        ]
        inherited = [d for sublist in inherited_lists for d in sublist]

        # associações locais da própria entidade
        local_all = self.query_local(e1=entity, rel=assoc_name)
        local_assocs = [d for d in local_all if isinstance(d.relation, Association)]

        self.query_result = inherited + local_assocs
        return self.query_result

    
    # def query(self, entity, assoc_name=None, visited=None):
    #     if visited is None:
    #         visited = set()

    #     # evitar loops em caso de ciclos na rede
    #     if entity in visited:
    #         return []
    #     visited.add(entity)

    #     # associações locais da própria entidade
    #     local_assocs = self.query_local(
    #         e1=entity,
    #         rel=assoc_name,
    #         rel_type=Association
    #     )

    #     # entidades "acima" (tipos/ascendentes) via Member ou Subtype
    #     parent_entities = [
    #         d.relation.entity2
    #         for d in self.declarations
    #         if isinstance(d.relation, (Member, Subtype))
    #         and d.relation.entity1 == entity
    #     ]

    #     # associações herdadas dos ascendentes
    #     inherited_assocs = []
    #     for p in parent_entities:
    #         inherited_assocs.extend(
    #             self.query(p, assoc_name, visited=visited)
    #         )

    #     result = inherited_assocs + local_assocs
    #     self.query_result = result
    #     return result

    #Devolve todas as declaracoes locaisou herdadas
    def query2(self, entity, rel_name=None):
        # associações locais + herdadas (já tratadas em query)
        assocs = self.query(entity, rel_name)

        # relações locais Member/Subtype da entidade
        local_all = self.query_local(e1=entity, rel=rel_name)
        local_ms = [d for d in local_all if isinstance(d.relation, (Member, Subtype))]

        self.query_result = assocs + local_ms
        return self.query_result


    #se a entidade tiver declarações locais dessa associação, não herda as dos predecessores.
    def query_cancel(self, entity, assoc_name):
        # declarações locais dessa associação (filtrar só Association)
        local_all = self.query_local(e1=entity, rel=assoc_name)
        local = [d for d in local_all if isinstance(d.relation, Association)]

        if local:
            return local

        # se não houver locais, herda dos predecessores
        pds = [
            self.query_cancel(d.relation.entity2, assoc_name)
            for d in self.declarations
            if isinstance(d.relation, (Member, Subtype))
            and d.relation.entity1 == entity
        ]

        return [decl for sublist in pds for decl in sublist]
    

    #Desce a hierarquia: de um tipo para subtipos e membros, e recolhe as associações nessa associação.
    def query_down(self, type_entity, assoc_name):
        descs = [
            d.relation.entity1
            for d in self.declarations
            if isinstance(d.relation, (Member, Subtype))
            and d.relation.entity2 == type_entity
        ]

        q_descs = [
            [d for d in self.query_local(e1=e, rel=assoc_name) if isinstance(d.relation, Association)]
            + self.query_down(e, assoc_name)
            for e in descs
        ]

        return [decl for sublist in q_descs for decl in sublist]

    #Dado um tipo e uma associacao, devolva o valor mais frequente dessa associacao nas entidades descendentes
    def query_induce(self, type_entity, assoc_name):
        decls = self.query_down(type_entity, assoc_name)
        vals = [d.relation.entity2 for d in decls]

        if not vals:
            return None

        return max(set(vals), key=vals.count)


    #recebe uma entidade e (o nome de) uma associacaao, e devolve o resultado -> consultas de valores das associacoes locais de uma dada entidade
    def query_local_assoc(self, entity, assoc_name):
        # todas as declarações locais dessa associação
        decls = [
            d for d in self.query_local(e1=entity, rel=assoc_name)
            if isinstance(d.relation, (Association, AssocOne, AssocNum))
        ]

        if not decls:
            return None

        rel0 = decls[0].relation

        # --- caso Association: multi-valor, lista de (val, freq) ---    
        if isinstance(rel0, Association):
            vals = [d.relation.entity2 for d in decls]
            total = len(vals)

            # contagens por valor
            counts = {}
            for v in vals:
                counts[v] = counts.get(v, 0) + 1

            # ordenar por frequência decrescente
            sorted_vals = sorted(counts.items(), key=lambda x: x[1], reverse=True)

            result = []
            acc = 0
            for v, c in sorted_vals:
                freq = c / total
                result.append((v, freq))
                acc += freq
                if acc >= 0.75:
                    break

            return result

        # --- caso AssocOne: devolver (val_mais_frequente, freq) ---
        if isinstance(rel0, AssocOne):
            vals = [d.relation.entity2 for d in decls]
            total = len(vals)

            counts = {}
            for v in vals:
                counts[v] = counts.get(v, 0) + 1

            best_val, best_count = max(counts.items(), key=lambda x: x[1])
            freq = best_count / total
            return (best_val, freq)

        # --- caso AssocNum: média dos valores ---
        if isinstance(rel0, AssocNum):
            nums = [d.relation.entity2 for d in decls]
            return sum(nums) / len(nums)


    #dada uma entidade e uma associacao devolva o valor dessa associacao nessa entidade (de acordo com uns criterios)
    def query_assoc_value(self, E, A):
        # 1) Declarações LOCAIS da associação A em E
        local = [
            d for d in self.query_local(e1=E, rel=A)
            if isinstance(d.relation, Association)
        ]
        local_values = [d.relation.entity2 for d in local]

        # Caso 1: todas as declarações locais atribuem o mesmo valor -> devolve esse valor
        if local_values and len(set(local_values)) == 1:
            return local_values[0]

        # 2) Declarações HERDADAS (só Association)
        all_decls = [
            d for d in self.query(E, A)
            if isinstance(d.relation, Association)
        ]
        predecessors = [d for d in all_decls if d not in local]
        predecessor_values = [d.relation.entity2 for d in predecessors]

        # Caso especial: não há locais nem herdadas -> não há valor
        if not local_values and not predecessor_values:
            return None

        # L(E,A,V): percentagem de declarações de V em E
        def L(value):
            if not local:
                return 0.0
            return len([d for d in local if d.relation.entity2 == value]) / len(local)

        # H(E,A,V): percentagem de declarações de V nas entidades predecessoras
        def H(value):
            if not predecessors:
                return 0.0
            return len([d for d in predecessors if d.relation.entity2 == value]) / len(predecessors)

        # candidatos: todos os valores locais e herdados
        candidates = set(local_values + predecessor_values)

        # Caso 2 + 3:
        # - se só locais: H = 0 -> max L/2 == max L
        # - se só herdadas: L = 0 -> max H/2 == max H
        # - se ambos: max (L+H)/2
        return max(
            candidates,
            key=lambda v: (L(v) + H(v)) / 2.0
    )