""" Obtain all kinds of attributes of a graph dataset.
"""


def get_dataset_attributes(Gn,
                           target=None,
                           attr_names=[],
                           node_label=None,
                           edge_label=None):
    import networkx as nx
    import numpy as np

    attrs = {}

    def get_dataset_size(Gn):
        return len(Gn)

    def get_all_node_num(Gn):
        return [nx.number_of_nodes(G) for G in Gn]

    def get_ave_node_num(all_node_num):
        return np.mean(all_node_num)

    def get_min_node_num(all_node_num):
        return np.amin(all_node_num)

    def get_max_node_num(all_node_num):
        return np.amax(all_node_num)

    def get_all_edge_num(Gn):
        return [nx.number_of_edges(G) for G in Gn]

    def get_ave_edge_num(all_edge_num):
        return np.mean(all_edge_num)

    def get_min_edge_num(all_edge_num):
        return np.amin(all_edge_num)

    def get_max_edge_num(all_edge_num):
        return np.amax(all_edge_num)

    def is_node_labeled(Gn):
        return False if node_label is None else True

    def get_node_label_num(Gn):
        nl = set()
        for G in Gn:
            nl = nl | set(nx.get_node_attributes(G, node_label).values())
        return len(nl)

    def is_edge_labeled(Gn):
        return False if edge_label is None else True

    def get_edge_label_num(Gn):
        el = set()
        for G in Gn:
            el = el | set(nx.get_edge_attributes(G, edge_label).values())
        return len(el)

    def is_directed(Gn):
        return nx.is_directed(Gn[0])

    def get_ave_node_degree(Gn):
        return np.mean([np.mean(list(dict(G.degree()).values())) for G in Gn])

    def get_max_node_degree(Gn):
        return np.amax([np.mean(list(dict(G.degree()).values())) for G in Gn])

    def get_min_node_degree(Gn):
        return np.amin([np.mean(list(dict(G.degree()).values())) for G in Gn])
    
    # get fill factor, the number of non-zero entries in the adjacency matrix.
    def get_ave_fill_factor(Gn):
        return np.mean([nx.number_of_edges(G) / (nx.number_of_nodes(G) 
                                           * nx.number_of_nodes(G)) for G in Gn])

    def get_max_fill_factor(Gn):
        return np.amax([nx.number_of_edges(G) / (nx.number_of_nodes(G) 
                                           * nx.number_of_nodes(G)) for G in Gn])

    def get_min_fill_factor(Gn):
        return np.amin([nx.number_of_edges(G) / (nx.number_of_nodes(G) 
                                           * nx.number_of_nodes(G)) for G in Gn])

    def get_substructures(Gn):
        subs = set()
        for G in Gn:
            degrees = list(dict(G.degree()).values())
            if any(i == 2 for i in degrees):
                subs.add('linear')
            if np.amax(degrees) >= 3:
                subs.add('non linear')
            if 'linear' in subs and 'non linear' in subs:
                break

        if is_directed(Gn):
            for G in Gn:
                if len(list(nx.find_cycle(G))) > 0:
                    subs.add('cyclic')
                    break
        # else:
        #     # @todo: this method does not work for big graph with large amount of edges like D&D, try a better way.
        #     upper = np.amin([nx.number_of_edges(G) for G in Gn]) * 2 + 10
        #     for G in Gn:
        #         if (nx.number_of_edges(G) < upper):
        #             cyc = list(nx.simple_cycles(G.to_directed()))
        #             if any(len(i) > 2 for i in cyc):
        #                 subs.add('cyclic')
        #                 break
        #     if 'cyclic' not in subs:
        #         for G in Gn:
        #             cyc = list(nx.simple_cycles(G.to_directed()))
        #             if any(len(i) > 2 for i in cyc):
        #                 subs.add('cyclic')
        #                 break

        return subs

    def get_class_num(target):
        return len(set(target))

    def get_node_attr_dim(Gn):
        for G in Gn:
            for n in G.nodes(data=True):
                if 'attributes' in n[1]:
                    return len(n[1]['attributes'])
        return 0

    def get_edge_attr_dim(Gn):
        for G in Gn:
            if nx.number_of_edges(G) > 0:
                for e in G.edges(data=True):
                    if 'attributes' in e[2]:
                        return len(e[2]['attributes'])
        return 0

    if attr_names == []:
        attr_names = [
            'substructures',
            'node_labeled',
            'edge_labeled',
            'is_directed',
            'dataset_size',
            'ave_node_num',
            'min_node_num',
            'max_node_num',
            'ave_edge_num',
            'min_edge_num',
            'max_edge_num',
            'ave_node_degree',
            'min_node_degree',
            'max_node_degree',
            'ave_fill_factor',
            'min_fill_factor',
            'max_fill_factor',
            'node_label_num',
            'edge_label_num',
            'node_attr_dim',
            'edge_attr_dim',
            'class_number',
        ]

    # dataset size
    if 'dataset_size' in attr_names:

        attrs.update({'dataset_size': get_dataset_size(Gn)})

    # graph node number
    if any(i in attr_names
           for i in ['ave_node_num', 'min_node_num', 'max_node_num']):

        all_node_num = get_all_node_num(Gn)

    if 'ave_node_num' in attr_names:

        attrs.update({'ave_node_num': get_ave_node_num(all_node_num)})

    if 'min_node_num' in attr_names:

        attrs.update({'min_node_num': get_min_node_num(all_node_num)})

    if 'max_node_num' in attr_names:

        attrs.update({'max_node_num': get_max_node_num(all_node_num)})

    # graph edge number
    if any(i in attr_names for i in
           ['ave_edge_num', 'min_edge_num', 'max_edge_num']):

        all_edge_num = get_all_edge_num(Gn)

    if 'ave_edge_num' in attr_names:

        attrs.update({'ave_edge_num': get_ave_edge_num(all_edge_num)})

    if 'max_edge_num' in attr_names:

        attrs.update({'max_edge_num': get_max_edge_num(all_edge_num)})

    if 'min_edge_num' in attr_names:

        attrs.update({'min_edge_num': get_min_edge_num(all_edge_num)})

    # label number
    if any(i in attr_names for i in ['node_labeled', 'node_label_num']):
        is_nl = is_node_labeled(Gn)
        node_label_num = get_node_label_num(Gn)

    if 'node_labeled' in attr_names:
        # graphs are considered node unlabeled if all nodes have the same label.
        attrs.update({'node_labeled': is_nl if node_label_num > 1 else False})

    if 'node_label_num' in attr_names:
        attrs.update({'node_label_num': node_label_num})

    if any(i in attr_names for i in ['edge_labeled', 'edge_label_num']):
        is_el = is_edge_labeled(Gn)
        edge_label_num = get_edge_label_num(Gn)

    if 'edge_labeled' in attr_names:
        # graphs are considered edge unlabeled if all edges have the same label.
        attrs.update({'edge_labeled': is_el if edge_label_num > 1 else False})

    if 'edge_label_num' in attr_names:
        attrs.update({'edge_label_num': edge_label_num})

    if 'is_directed' in attr_names:
        attrs.update({'is_directed': is_directed(Gn)})

    if 'ave_node_degree' in attr_names:
        attrs.update({'ave_node_degree': get_ave_node_degree(Gn)})

    if 'max_node_degree' in attr_names:
        attrs.update({'max_node_degree': get_max_node_degree(Gn)})

    if 'min_node_degree' in attr_names:
        attrs.update({'min_node_degree': get_min_node_degree(Gn)})
        
    if 'ave_fill_factor' in attr_names:
        attrs.update({'ave_fill_factor': get_ave_fill_factor(Gn)})

    if 'max_fill_factor' in attr_names:
        attrs.update({'max_fill_factor': get_max_fill_factor(Gn)})

    if 'min_fill_factor' in attr_names:
        attrs.update({'min_fill_factor': get_min_fill_factor(Gn)})

    if 'substructures' in attr_names:
        attrs.update({'substructures': get_substructures(Gn)})

    if 'class_number' in attr_names:
        attrs.update({'class_number': get_class_num(target)})

    if 'node_attr_dim' in attr_names:
        attrs['node_attr_dim'] = get_node_attr_dim(Gn)

    if 'edge_attr_dim' in attr_names:
        attrs['edge_attr_dim'] = get_edge_attr_dim(Gn)

    from collections import OrderedDict
    return OrderedDict(
        sorted(attrs.items(), key=lambda i: attr_names.index(i[0])))
