import networkx as nx
import networkx
from collections import defaultdict
import logging
import sys
import traceback
import time
from joblib import Parallel, delayed
import warnings

from text2graphapi.src.Utils import Utils
from text2graphapi.src.Preprocessing import Preprocessing
from text2graphapi.src.GraphTransformation import GraphTransformation
from text2graphapi.src import Graph

# Configs
TEST_API_FROM = 'PYPI' #posible values: LOCAL, PYPI
warnings.filterwarnings("ignore")
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(asctime)s; - %(levelname)s; - %(message)s")
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


'''# TEST API PYPI
if TEST_API_FROM == 'PYPI':
    from text2graphapi.src.Utils import Utils
    from text2graphapi.src.Preprocessing import Preprocessing
    from text2graphapi.src.GraphTransformation import GraphTransformation
    from text2graphapi.src import Graph
# TEST API LOCAL
else:
    from src.Utils import Utils
    from src.Preprocessing import Preprocessing
    from src.GraphTransformation import GraphTransformation
    from src import Graph'''
    

class Cooccurrence(Graph.Graph):
    """This module generate a word-coocurrence graph from raw text 
        
        :param str graph_type: graph type to generate, default=Graph (types: Graph, DiGraph, MultiGraph, MultiDiGraph)
        :param str output_format: output format to the graph default=networkx (formats: networkx, adj_matrix, adj_list, adj_pandas)
        :param int window_size: windows size for co-occurrence, default=1
        :param int language: language for text prepocessing, default=en (lang: en, es)
        :param bool apply_prep: flag to exec text prepocessing, default=true
        :param bool parallel_exec: flag to exec tranformation in parallel, default=false
    """
    def __init__(self, graph_type, output_format='', apply_preprocessing=True, window_size=1, parallel_exec=True, language='en', steps_preprocessing={}):
        """Constructor method
        """
        self.apply_prep = apply_preprocessing
        self.parallel_exec = parallel_exec
        self.window_size = window_size
        self.prep = Preprocessing(lang=language, steps_preprocessing=steps_preprocessing)
        self.utils = Utils()
        self.graph_trans = GraphTransformation()
        self.graph_type = graph_type
        self.output_format = output_format
        #super().__init__(graph_type, output_format)


    # normalize text
    def _text_normalize(self, text: str) -> list:
        if self.apply_prep == True:
            #self.prep.prepocessing_pipeline(text)
            text = self.prep.handle_blank_spaces(text)
            text = self.prep.handle_non_ascii(text)
            text = self.prep.handle_emoticons(text)
            text = self.prep.handle_html_tags(text)
            text = self.prep.handle_stop_words(text)
            text = self.prep.to_lowercase(text)
            #text = self.prep.handle_blank_spaces(text)

        #preproc baseline: word_tokenize
        #text = self.prep.word_tokenize(text)
        return text


    # get nodes an its attributes
    def _get_entities(self, text_doc: list) -> list:  
        nodes = []
        word_tokens_tags = self.prep.pos_tagger(text_doc)
        for n in word_tokens_tags:
            node = (str(n[0]), {'pos_tag': n[1]}) # (word, {'node_attr': value})
            nodes.append(node)

        logger.debug("Nodes: %s", nodes)
        return nodes


    # get edges an its attributes
    def _get_relations(self, text_doc: list) -> list:
        text_doc_tokens = self.prep.word_tokenize(text_doc)
        edges = []
        d_cocc = defaultdict(int)
        for i in range(len(text_doc_tokens)):
            word = text_doc_tokens[i]
            next_word = text_doc_tokens[i+1 : i+1 + self.window_size]
            for t in next_word:
                key = (word, t)
                d_cocc[key] += 1
        for key, value in d_cocc.items():
            edge = (key[0], key[1], {'freq': value})  # (word_i, word_j, {'edge_attr': value})
            edges.append(edge) 

        logger.debug("Edges: %s", edges)
        return edges
    

    # build nx-graph based of nodes and edges
    def _build_graph(self, nodes: list, edges: list) -> networkx:
        # pending validations
        graph = super().set_graph_type(self.graph_type)
        graph.add_nodes_from(nodes)
        graph.add_edges_from(edges)
        return graph


    def _transform_pipeline(self, text_instance: list) -> list:
        output_dict = {
            'doc_id': text_instance['id'], 
            'graph': None, 
            'number_of_edges': 0, 
            'number_of_nodes': 0, 
            'status': 'success'
        }
        try:
            #1. text preprocessing
            prep_text = self._text_normalize(text_instance['doc'])
            #2. get_entities
            nodes = self._get_entities(prep_text)
            #3. get_relations
            edges = self._get_relations(prep_text)
            #4. build graph
            graph = self._build_graph(nodes, edges)
            output_dict['number_of_edges'] += graph.number_of_edges()
            output_dict['number_of_nodes'] += graph.number_of_nodes()
            #5. graph_transformation
            output_dict['graph'] = self.graph_trans.transform(self.output_format, graph)
        except Exception as e:
            logger.error('Error: %s', str(e))
            logger.debug('Error Detail: %s', str(traceback.format_exc()))
            output_dict['status'] = 'fail'
        finally:
            return output_dict
        

    # tranform raw_text to graph based on input params
    def transform(self, corpus_texts) -> list:
        """
        This method transform input raw_text to graph representation

        :param list corpus_texts: input texts to transform into graphs 
        
        :return: list
        
        Input Example

        .. highlight:: python
        .. code-block:: python
            
            corpus_texts = [
                {  
                    "id": 1, 
                    "doc": "text_data"
                }, ...
            ]
        
        Output Example

        .. highlight:: python
        .. code-block:: python
            
            [
                {
                    "id": 1, 
                    "doc_graph": adj_matrix, 
                    'number_of_edges': 123, 
                    'number_of_nodes': 321 
                    'status': 'success'
                }, ...
            ]

            

        """
        logger.info("Init transformations: Text to Co-Ocurrence Graph")
        logger.info("Number of Text Documents: %s", len(corpus_texts))
        corpus_output_graph, delayed_func = [], []

        for input_text in corpus_texts:
            logger.debug('--- Processing doc %s ', str(input_text['id'])) 
            if self.parallel_exec == True: 
                delayed_func.append(
                    self.utils.joblib_delayed(funct=self._transform_pipeline, params=input_text) 
                )
            else:
                corpus_output_graph.append(self._transform_pipeline(input_text))

        if self.parallel_exec == True: 
            corpus_output_graph = Parallel(n_jobs=-1)(delayed_func)
        
        logger.info("Done transformations")
        return corpus_output_graph

