"""!
Contains base functions to use into and outside Grafon package.
"""

import logging
import os, sys
import operator
import networkx
import unicodedata
import grafon.constants as constants

from grafon.utils import sumoproc
from grafon.utils import plwnproc
from grafon.graphs.basegraph import BaseGraph
from grafon.mergers.basegraphmerger import BaseGraphMerger
from grafon.structures import TextNodes
from grafon.measures.sidddescriptormeasure import SIDDDescriptorMeasure

import argparse
try:
    import argcomplete
except ImportError:
    argcomplete = None

def make_parser(desc = ''):
  """!
  Makes base parser containing only path to config, visual dir and decision
  about merging graphs into one big.
  
  @param desc: Parser descirpt
  """
  aparser = argparse.ArgumentParser(
    description = desc,
    formatter_class=argparse.RawDescriptionHelpFormatter)
  aparser.add_argument('-c', '--config-file', dest='config',
                       default='cfg/grafon.ini',
                       help='Grafon config file')
  aparser.add_argument('-gd', '--out-graph-dir', dest='out_graph_dir',
                       required=False, help=(
                         'If path is given, then created graphs will are '
                         'stored into this directory. Each sentence into '
                         'separated files. If -m is set, then the whole '
                         'graph will be stored into one file'))
  aparser.add_argument('-of', '--out-graph-format', dest='out_graph_format',
                       required=False, default='pickle', help=(
                         'Output graph format. One of <pickle, json>. '
                         'Default is pickle'))
  
  aparser.add_argument('-vd', '--visualise-dir', dest='visualise_dir',
                       required=False, help=(
                         'Visualisation dir. If is given, '
                         'then into it the output graph will be stored.'))
  aparser.add_argument('-vf', '--visualise-format', dest='visualise_format',
                       required=False, help=(
                         'Format of images which will be stored. As default '
                         'is set dot. Possible options are: dot, png'),
                       default='dot')
  
  aparser.add_argument('-m', '--merge-sentences', dest='merge_sents',
                       action='store_true', default=False, help=(
                         'If parameter is used then all graphs into '
                         'both ccl files will be merged into one.'))
  aparser.add_argument('--verbose', dest='verbose_mode', action='store_true',
                       help=('If option is enabled then grafon '
                         'is more verbose.'))
  if argcomplete:
    argcomplete.autocomplete(aparser)
  return aparser

def write_graph_to_img_file(g, outfile, oformat):
  """!
  Function to store given graph to file as png image.
  Given graph is converted to agraph.

  @param g grap to draw
  @type g: Any of networkx.graph
  @param outfile Path to output filename
  @type outfile: String
  """
  try:
    mg = networkx.MultiDiGraph()
    mg.add_nodes_from(g.nodes())
    A = networkx.to_agraph(g)
    A.layout(prog='dot', args="-Grankdir=LR -Goverlap=prism")
    ofname = '{}.{}'.format(outfile, oformat)
    A.draw(ofname)
  except Exception, e:
    logging.error("ERROR: graph identifier {}".format(outfile))
    logging.error(e)

def convert_g_with_plwnsynstr(g):
  """!
  Converts given graph g to new where instead of plWN synset 
  identifiers are string-like lexical units.
  """
  newg = networkx.MultiDiGraph()
  for n in g.nodes():
    label = ''
    in_edges = g.in_edges(n, data=True)
    out_edges = g.out_edges(n, data=True)
    if constants.NODE_FROM_PLWN in g.node[n][constants.NODE_SOURCES]:
      lbl = g.node[n][constants.NODE_PLWN_STR].strip()
      if not len(lbl):
        label = n
      else:
        label = lbl
    else:
      label = n
    for fn, tn, d in in_edges:
      newg.add_edge(fn, label, attr_dict = d)
    for fn, tn, d in out_edges:
      newg.add_edge(label, tn, attr_dict = d)
  return newg

def vis_graphs_into_files(bg, graphs, outdir, oformat,
                          plwn_to_str=False, ):
  """!
  Saves all given graphs into image files. Images are stored into given path 
  of directory, when the directory not exists, then is cteated.

  Additionaly if plwn_to_str is set to True then the graph is converted to new
  where instead of plWN synset identifiers are string-like lexical units.

  @param bg: BaseGraph
  @param graphs: list of graphs to save into file
  @param outdir: Directory when the graphs will are stored.
  @param plwn_to_str: marker to convert plwn-node labels to synset string
  """
  logging.info('Saving img graphs for file {}'.format(
    bg.document.path().split(';')[0]))

  if not os.path.exists(outdir):
    os.makedirs(outdir)

  doc_name = os.path.basename(bg.document.path().split(';')[0])
  for sid, g in graphs:
    if plwn_to_str:
      try:
        g = convert_g_with_plwnsynstr(g)
      except Exception, e:
        logging.error("ERROR {}: {}".format(sid, e))
    ofile_name =  '%s-%s' % (doc_name, sid)
    outfile = os.path.join(outdir, ofile_name)
    write_graph_to_img_file(g, outfile, oformat)

def write_graph_to_file(g, outfile, outformat):
  for n in g.nodes():
    g.node[n][constants.NODE_TOKENS] = []
  for fn, tn, d in g.edges(data=True):
    if d.has_key('rel_name'):
      d['rel_name'] = ''

  if outformat == 'pickle':
    networkx.write_gpickle(g, outfile)
  elif outformat == 'json':
    import json
    from networkx.readwrite import json_graph
    data = json_graph.node_link_data(g)
    with open(outfile, 'w') as ofile:
      json.dump(data, ofile, sort_keys=True, indent=4, separators=(',', ': '))
  else:
    logging.error('No output format specified!')

def save_graphs_into_files(bg, graphs, outdir, oformat):
  """!
  Saves all given graphs into files. 
  When the directory not exists, then is cteated.
  
  @param bg: BaseGraph
  @param graphs: list of graphs to save into file
  @param outdir: Directory when the graphs will are stored.
  """
  logging.info('Saving pickled graphs for file {}'.format(
      bg.document.path().split(';')[0]))

  if not os.path.exists(outdir):
    os.makedirs(outdir)
  doc_name = os.path.basename(bg.document.path().split(';')[0])
  for sid, g in graphs:
    ofile_name =  '{}#{}.{}'.format(doc_name, sid, oformat)
    outfile = os.path.join(outdir, ofile_name)
    write_graph_to_file(g, outfile, oformat)

def merge_graph(gk, graph, sgraph, plwngraph, pmerger, smerger, reducers):
  """!
  Merge single graph from basegraph dictionary with SUMO and/or plWordNet
  @param smerger: instance of sumo merger
  @param sgraph: sumo graph (whole graph)
  @param pmerger: instance pf plwordnet merger
  @param plwngraph: plwordnet graph (whole)
  @param reducers: List of reducers
  @return Pair of graph identifier and merged graph
  """
  ng = None
  # if any plwordnet merger has been used
  if pmerger:
      senses = plwnproc.get_senses_from_nxgraph(graph)
      in_text = TextNodes(senses)
      logging.info('Reducing and merging plWordNet graph - {}'.format(gk))
      sense_sub = plwngraph.reducers_chain(in_text, reducers)
      ng = pmerger.merge(ng if ng else graph, sense_sub)
  # if any of sumo merger should be used
  if smerger:
      concepts  = sumoproc.get_concepts_from_nxgraph(graph)
      in_text = TextNodes(concepts)
      logging.info('Reducing and merging SUMO graph - {}'.format(gk))
      concep_sub = sgraph.reducers_chain(in_text, reducers)
      ng = smerger.merge(ng if ng else graph, concep_sub)
  return (gk, ng if ng else graph)

def merge_graphs(bg, smerger, sgraph, pmerger, plwngraph, reducers):
  """!
  @param smerger: instance of sumo merger
  @param sgraph: sumo graph (whole graph)
  @param pmerger: instance pf plwordnet merger
  @param plwngraph: plwordnet graph (whole)
  @param reducers: List of reducers
  """
  return [
    merge_graph(gk, graph, sgraph, plwngraph, pmerger, smerger, reducers) \
      for gk, graph in bg.graph_dict.iteritems()]

def merge_graphs_in_proc(bg, smerger, sgraph, pmerger, 
                         plwngraph, reducers, njobs):
  """!
  Merge graphs into the process

  @param smerger: instance of sumo merger
  @param sgraph: sumo graph (whole graph)
  @param pmerger: instance pf plwordnet merger
  @param plwngraph: plwordnet graph (whole)
  @param reducers: List of reducers
  @param njobs: number of jobs
  """
  raise Exception((
    'Parallel processing is not implemented yet!\n'
    'Please run the application without option -n!'))

def merge_graphs_to_dict(bg, smerger, sgraph, pmerger, plwngraph, reducers):
  """!
  @param smerger: instance of sumo merger
  @param sgraph: sumo graph (whole graph)
  @param pmerger: instance pf plwordnet merger
  @param plwngraph: plwordnet graph (whole)
  @param reducers: List of reducers
  """
  all_graphs = dict()
  for gk, graph in bg.graph_dict.iteritems():
    ng = None
    # if any plwordnet merger has been used
    if pmerger:
      senses = plwnproc.get_senses_from_nxgraph(graph)
      in_text = TextNodes(senses)
      sense_sub = plwngraph.reducers_chain(in_text, reducers)
      ng = pmerger.merge(ng if ng else graph, sense_sub)
    # if any of sumo merger should be used
    if smerger:
      concepts  = sumoproc.get_concepts_from_nxgraph(graph)
      in_text = TextNodes(concepts)
      concep_sub = sgraph.reducers_chain(in_text, reducers)
      ng = smerger.merge(ng if ng else graph, concep_sub)
    all_graphs[gk] = ng if ng else graph
  return all_graphs

def read_sumo_graph(cfg):
  """!

  @param cfg: Config file
  """
  from grafon.graphs.sumograph import SumoGraph
  print >> sys.stderr, "-------------------------------------------------------------"
  print >> sys.stderr, "WARNING!!"
  print >> sys.stderr, "grafon_base.read_sumo_graph will not be longer supported!"
  print >> sys.stderr, "Instead of this method you sould to use:"
  print >> sys.stderr, ""
  print >> sys.stderr, "         bg.resources[constants.RESOURCES_SUMO_HNDL]"
  print >> sys.stderr, ""
  print >> sys.stderr, "-------------------------------------------------------------"
  sumo_graph_file = cfg.sumo_graph_csv()
  sgraph = SumoGraph()
  sgraph.build_graph(sumo_graph_file)
  print >> sys.stderr, ''
  return sgraph

def read_plwn_graph(cfg):
  """!
  
  @param cfg: Config file
  """
  from grafon.graphs.plwngraph import PlWNGraph
  print >> sys.stderr, "-------------------------------------------------------------"
  print >> sys.stderr, "WARNING!!"
  print >> sys.stderr, "grafon_base.read_plwn_graph will not be longer supported!"
  print >> sys.stderr, "Instead of this method you sould to use:"
  print >> sys.stderr, ""
  print >> sys.stderr, "         bg.resources[constants.RESOURCES_PLWN_HNDL]"
  print >> sys.stderr, ""
  print >> sys.stderr, "-------------------------------------------------------------"

  dbcfg_file  = cfg.plwn_db_config()
  out_pickle = cfg.plwn_pickle()
  plwngraph = PlWNGraph()
  if not plwngraph.read_graph(dbcfg_file, out_pickle):
    logging.error('Cannot connect to database!')
    exit(1)
  print >> sys.stderr, ''
  return plwngraph


def merge_graphs_into_one(bg, gid, graphs):
  """!
  Merge all graphs from basegraph to one bigger.

  @param bg Object of BaseGraph
  @type bg: grafon.graphs.Basegraph

  @param gid graph identifier
  @type gid: String

  @param  graphs List of graphs to merge into one
  @type graphs: List of any of networkx.Graph
  """
  if not graphs or not bg:
    return None
  if len(graphs) < 2:
    return graphs
  bm = BaseGraphMerger(bg)
  mg = graphs[0][1]
  for i in range(1, len(graphs)):
    mg = bm.merge(mg, graphs[i][1])
  return [(gid, mg)]

def print_sentences(bg, GID):
  """!
  Based on the given base graph prints the sentences from dictionary into bg.
  
  @param bg Base graph with dictionary of sentences
  @param GID graph identifier
  """
  for p in bg.document.paragraphs():
    for s in p.sentences():
      print >> sys.stderr, "%s(%s); \"%s\"" % ( 
        GID, s.id(), ' '.join(t.orth_utf8() for t in s.tokens()))

def run_pagerank_for_graphs(all_graphs):
  """!
  For given graphs runs pagerank algorithm
  
  @param all_graphs: list of graphs built from the text
  """
  for g in all_graphs:
    prg = networkx.DiGraph()
    prg.add_edges_from(g[1].edges(), data=True)
    pr = networkx.pagerank(prg, max_iter=200)
    sorted_x = sorted(pr.items(), key=operator.itemgetter(1), reverse=True)
    for (k, v) in sorted_x:
      print k, v

def build_graph_for_file(cclfilepath, relcclfilepath, bgbuilder, 
                         tagset, node, filters):
  """!
  For the given ccl file (and also relcclfile) build  the Basegraph and 
  return it.

  Built BaseGraph for each sentence from cclfile contains the separated gaph.

  @param cclfilepath
  @type cclfilepath:
  @param relcclfilepath
  @type relcclfilepath:
  @param bgbuilder Object of builder
  @type bgbuilder: GraphBuilder()
  @param tagset object of tagset
  @type tagset: corpus2.Tagset()
  @param node
  @type node: GrafonConfig.node(tagset)
  @param filters
  @type filters: list of filters: GrafonConfig.filters(tagset)
  @return base graph for the given file.
  @rtype: BaseGraph
  """
  bg = BaseGraph(node, filters)
  bgbuilder.build_graph_from_file(bg, cclfilepath, relcclfilepath, tagset)
  return bg

def calculate_sim_between_all_nodes(cfg, all_graphs, output_file):
  """!
  Calculates similarity between all nodes.

  Store result into file
  """
  with open(output_file, 'wt') as fout:
    # dla kazdej miary
    for nmeasure in cfg.node_measures():
      # dla kazdego grafu
      for (gid, g) in all_graphs:
        gn1 = g.nodes()
        gn2 = g.nodes()
        # header
        fout.write(';')
        for n in gn1:
          fout.write('%s:%s;' % (str(nmeasure), n.encode(constants.ENCODING)))
        fout.write('\n')
        for n2 in g.nodes():
          fout.write('%s;' % n2.encode(constants.ENCODING))
          for n1 in g.nodes():
            fout.write('%f;' % nmeasure.calculate_sim_nodes(g, n1, g, n2))
          fout.write('\n')

def filter_n_e_by_count(cfg, all_graphs):
  for g in all_graphs:
    for n in g[1].nodes():
      if g[1].node[n].has_key(constants.NODE_COUNT) and \
        g[1].node[n][constants.NODE_COUNT] < cfg.min_node_count():
        g[1].remove_node(n)
    for (fn, tn) in g[1].edges():
      for k, data in g[1][fn][tn].iteritems():
        if data.has_key(constants.NODE_COUNT) and \
          data[constants.NODE_COUNT] < cfg.min_edge_count():
          g[1].remove_edge(fn, tn, key=k)
          break
