#
# <one line to give the library's name and an idea of what it does.>
# Copyright (C) 2014  <copyright holder> <email>
# 
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# 
#

import importlib
import re
import logging
import grafon.constants as constants

CFG_SEC_GRAFON = 'grafon'
CFG_SEC_OPTIONS_FILTER  = '{}:filters_options'.format(CFG_SEC_GRAFON)
CFG_SEC_OPTIONS_MEASURE = '{}:measure_options'.format(CFG_SEC_GRAFON)
CFG_SEC_OPTIONS_RECUCER = '{}:reducer_options'.format(CFG_SEC_GRAFON)
CFG_SEC_OPTIONS_ADDONS  = '{}:addons'.format(CFG_SEC_GRAFON)

CFG_SEC_PLWN = 'plwn'
CFG_SEC_SUMO = 'sumo'
CFG_SEC_MSR  = 'msr'

CFG_GRAFON_OPT_REBUILDERS = 'rebuilders'
CFG_GRAFON_OPT_EBUILDERS = 'ebuilders'
CFG_GRAFON_OPT_MEASURES = 'measures'
CFG_GRAFON_OPT_NODE_MEASURES = 'node_sim_measures'
CFG_GRAFON_OPT_REDUCERS = 'reducers'
CFG_GRAFON_OPT_FILTERS = 'filters'
CFG_GRAFON_OPT_PLWN_MERGER = 'plwn_merger'
CFG_GRAFON_OPT_SUMO_MERGER = 'sumo_merger'
CFG_GRAFON_OPT_NODE = 'node'
CFG_GRAFON_OPT_TAGSET = 'tagset'
CFG_GRAFON_OPT_LOG_FILE = 'log_filename'
CFG_GRAFON_OPT_MSR_PATH = 'msr_file'
CFG_GRAFON_OPT_LOG_LEVEL = 'log_level'
CFG_GRAGON_OPT_MIN_E_COUNT = 'min_edge_count'
CFG_GRAGON_OPT_MIN_N_COUNT = 'min_node_count'
CFG_PLWN_OPT_DBCONFIG = 'dbconfig'
CFG_PLWN_OPT_OUTPICKLE = 'outpickle'
CFG_GRAFON_OPT_ACCEPTED_RELATIONS = 'accepted_relations'

CFG_SUMO_OPT_CSV = 'sumo_graph_csv'

IMPORT_MERGERS = 'grafon.mergers'
IMPORT_FILTERS = 'grafon.filters'
IMPORT_NODES = 'grafon.nodes'
IMPORT_MEASURES = 'grafon.measures'
IMPORT_GRAPHS_REDUCERS = 'grafon.graphs.reducers'
IMPORT_EBUILDERS = 'grafon.ebuilders'
IMPORT_REBUILDERS = 'grafon.gbuilders'

class GrafonConfig:
  """!
  Base class represents parsed config from grafon.
  
  In this class are stored all variables from config file. It is possible to
  parse config file of course.
  """

  def __init__(self, fname = ''):
    """!
    Constructor makes config parser and sets all members to default values.
    
    @param fname: Path to config filename. If not given then path is set
                  to empty string.
    """
    from ConfigParser import ConfigParser
    self.__config = ConfigParser()
    #! config filename
    self.__filename = fname
    #! list of edge builders
    self.__ebuilders = []
    #! list of graph rebuilders
    self.__rebuilders = []
    #! list of all filters
    self.__filters = []
    self.__plwn_merger = ''
    self.__sumo_merger = ''
    #! list of all graph's reducers
    self.__reducers = []
    #! node representation
    self.__node = ''
    #! tagset used into application
    self.__tagset = None
    #! list of measures
    self.__measures = []
    self.__node_measures = []
    #! plwn database config file
    self.__plwn_db_config = ''
    #! plwn pickle file
    self.__plwn_pickle = ''
    #! MSR handle
    self.__msr_handle = None
    #! sumo graph in csv
    self.__sumo_graph_csv = ''
    #! measures' options, dict of dicts, a key is the name of a measure
    self.__measures_options = {}
    self.__node_measures_options = {}
    #! reducers' options, dict of dicts, a key is the name of a reducer
    self.__reducers_options = {}
    #! filters' options, dict of dicts, a key is the name of a filter
    self.__filters_options = {}
    #! Accepted relations (entailment relations)
    self.__accepted_relations = []

    self.__min_e_count = 1
    self.__min_n_count = 1

    if len(fname):
      self.read(fname)
  
  def ebuilders(self):
    """!
    @return edge builders
    """
    return self.__ebuilders
  
  def rebuilders(self):
    """!
    @return graph rebuilders
    """
    return self.__rebuilders

  def filters(self, tagset):
    """!
    Return the list of filter objects.
    
    @param tagset: object of corpus2.Tagset
    @return list of object of Filters
    """
    if not self.__filters: 
      for filter_name, options in self.__filters_options.iteritems():
        module = __import__(IMPORT_FILTERS, fromlist = [filter_name])
        self.__filters.append(getattr(module, filter_name)(tagset, options))
    return self.__filters

  def plwn_merger(self, bg):
    """!
    From config file reads what plWordNet merger will be used.
    Makes this object and returns it.
    
    @param bg: Object of BaseGraph
    @return object of MergerInterface
    """
    if self.__plwn_merger:
      module = __import__(
        IMPORT_MERGERS, 
        fromlist = [self.__plwn_merger])
      return getattr(module, self.__plwn_merger)(bg)
    return None

  def sumo_merger(self, bg):
    """!
    From config file reads what SUMO merger will be used.
    Makes this object and returns it.
    
    @param bg: Object of BaseGraph
    @return object of MergerInterface
    """
    if self.__sumo_merger:
      module = __import__(
        IMPORT_MERGERS, 
        fromlist = [self.__sumo_merger])
      return getattr(module, self.__sumo_merger)(bg)
    return None
  
  def node(self, tagset):
    """!
    Checks what node converter will be used in the process.
    
    @param taset: Object of corpus2.Tagset
    @return Object of NodeInterface
    """
    if self.__node:
      module = __import__(IMPORT_NODES, fromlist = [self.__node])
      return getattr(module, self.__node)(tagset)
    return None
  
  def min_node_count(self):
    """!
    Returns minimum number of node count into graph g
    """
    return self.__min_n_count

  def min_edge_count(self):
    """!
    Returns minimum number of edge count into graph g
    """
    return self.__min_e_count

  def tagset(self):
    """!
    Returns tagset object

    @return object of corpus2.Tagset
    """
    return self.__tagset
  
  def measures(self):
    """!
    Returns list of measures which will be used while processing.

    @return list of measures (MeasureInterface)
    """ 
    if not self.__measures: 
      for measure_name, options in self.__measures_options.iteritems():
        module = __import__(
          IMPORT_MEASURES, 
          fromlist = [measure_name])
        self.__measures.append(getattr(module, measure_name)(options))
    return self.__measures
  
  def node_measures(self):
    """!
    Returns list of measures between two nodes.

    @return list of measures (MeasureInterface)
    """ 
    if not self.__node_measures: 
      for measure_name, options in self.__node_measures_options.iteritems():
        module = __import__(
          IMPORT_MEASURES, 
          fromlist = [measure_name])
        self.__node_measures.append(getattr(module, measure_name)(options))
    return self.__node_measures
 
  def measure_options(self, str_measure):
    """!
    For given measure name returns its options.

    If measure options has not been read then returns None
    """
    return self.__measures_options.get(str_measure, None)
  
  def node_measure_options(self, str_measure):
    """!
    For given measure name returns its options.

    If measure options has not been read then returns None
    """
    return self.__node_measures_options.get(str_measure, None)

  def reducers(self):
    """!
    Returns list of reducers which will be used while processing.

    @return list of reducers (ReducerInterface)
    """ 
    if not self.__reducers: 
      for reducer_name, options in self.__reducers_options.iteritems():
        module = __import__(
          IMPORT_GRAPHS_REDUCERS, 
          fromlist = [reducer_name])
        self.__reducers.append(getattr(module, reducer_name)(options))
    # load classes in a given sequence
    elif isinstance(self.__reducers[0], (str, unicode)):
      for i, reducer_name in enumerate(self.__reducers):
        module = __import__(
          IMPORT_GRAPHS_REDUCERS, 
          fromlist = [reducer_name])
        self.__reducers[i] = \
          getattr(module, reducer_name)(self.__reducers_options[reducer_name])
    return self.__reducers

  def plwn_db_config(self):
    """!
    Returns path to database config in WordNetLoom format.

    @return path to config.
    """
    return self.__plwn_db_config
  
  def msr_handle(self):
    """
    Returns handle of MSR file
    
    @return MSRHandl object
    """
    return self.__msr_handle

  def plwn_pickle(self):
    """!
    Returns path to plWordnet pickle.

    @return path to pickle.
    """
    return self.__plwn_pickle

  def sumo_graph_csv(self):
    return self.__sumo_graph_csv
  
  def accepted_relations(self):
    """
    Accepted relations
    """
    if not len(self.__accepted_relations):
      # entailment relations
      self.__accepted_relations = self.__config.get(
        CFG_SEC_OPTIONS_ADDONS, 
        CFG_GRAFON_OPT_ACCEPTED_RELATIONS).strip().split()
    return self.__accepted_relations

  def read(self, filename):
    """!
    This method parse the given config file and makes required objects.

    @param filename: path to config file to parse it.
    """
    self.__filename = filename

    with open(self.__filename, 'r') as ini_file:
      self.__config.readfp(ini_file)

    # logging
      log_filename = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_LOG_FILE)
      if log_filename:
        logging.basicConfig(
          filename = log_filename, 
          level = self._get_log_level(
            self.__config.get(
              CFG_SEC_GRAFON,
              CFG_GRAFON_OPT_LOG_LEVEL)
            )
        )
      else:
        logging.basicConfig(
          level = self._get_log_level(
            self.__config.get(
              CFG_SEC_GRAFON,
              CFG_GRAFON_OPT_LOG_LEVEL)
            )
        )

      # convert edge builders to class representation
      self.__ebuilders = []
      ebuilders = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_EBUILDERS).split()
      for eb in ebuilders:
        module = __import__(IMPORT_EBUILDERS,fromlist = [eb])
        self.__ebuilders.append(getattr(module, eb)())

      # rebuilders
      self.__rebuilders = []
      rebuilders = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_REBUILDERS).split()
      for rb in rebuilders:
        module = __import__(IMPORT_REBUILDERS,fromlist = [rb])
        self.__rebuilders.append(getattr(module, rb)())

      # a single plwn mapper
      self.__plwn_merger = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_PLWN_MERGER)
      if len(self.__plwn_merger) < 1:
        self.__plwn_merger = None

      # a single sumo mapper
      self.__sumo_merger = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_SUMO_MERGER)
      if len(self.__sumo_merger) < 1:
        self.__sumo_merger = None

      # node representations
      self.__node = self.__config.get(
        CFG_SEC_GRAFON, 
        CFG_GRAFON_OPT_NODE)
      if len(self.__node) < 1:
        self.__node = None

      # tagset
      import corpus2
      self.__tagset = corpus2.get_named_tagset(
        self.__config.get(
          CFG_SEC_GRAFON, 
          CFG_GRAFON_OPT_TAGSET))

      # filters
      self.__filters_options, _ = self._load_options(
        CFG_GRAFON_OPT_FILTERS,
        CFG_SEC_OPTIONS_FILTER)

      # measures
      self.__measures_options, _ = self._load_options(
        CFG_GRAFON_OPT_MEASURES, 
        CFG_SEC_OPTIONS_MEASURE)
      self.__node_measures_options, _ = self._load_options(
        CFG_GRAFON_OPT_NODE_MEASURES, 
        CFG_SEC_OPTIONS_MEASURE)

      # reducers
      self.__reducers_options, self.__reducers = self._load_options(
        CFG_GRAFON_OPT_REDUCERS, 
        CFG_SEC_OPTIONS_RECUCER)

      if self.__plwn_merger or self.__sumo_merger:
        if not len(self.__reducers):
          logging.error(
            'If any merger has been set in grafon config, then a reducer '
            '(for example SimpleReducer) should be also set in config file!')
          exit(1)

      self.__plwn_db_config = self.__config.get(
        CFG_SEC_PLWN, 
        CFG_PLWN_OPT_DBCONFIG)
      self.__plwn_pickle = self.__config.get(
        CFG_SEC_PLWN, 
        CFG_PLWN_OPT_OUTPICKLE)

      self.__sumo_graph_csv = self.__config.get(
        CFG_SEC_SUMO, 
        CFG_SUMO_OPT_CSV)

      msr_path = self.__config.get(CFG_SEC_MSR, CFG_GRAFON_OPT_MSR_PATH)
      if len(msr_path):
        import msr
        self.__msr_handle = msr.MSRHandl(msr_path)

      self.__min_e_count = int(self.__config.get(
        CFG_SEC_OPTIONS_FILTER,
        CFG_GRAGON_OPT_MIN_E_COUNT))
      self.__min_n_count = int(self.__config.get(
        CFG_SEC_OPTIONS_FILTER,
        CFG_GRAGON_OPT_MIN_N_COUNT))

  def _load_options(self, sequence_name, options_section_name):
    """!
    Load options for classes in a specific sequence.

    The 'sequence_name' is the name of the option from the section 'grafon'
    that contains a sequence of classes. These classes may have additional 
    paramenters in order to run properly. The paramenters are stored in another 
    section (equals to 'options_section_name'). Each option has name
    TheNameOfTheClass_TheNameOfAnAttribute and a value.
    This method creates a dictionary whose keys are equal to the classes' names
    and values are options' dictionaries that are expected by these classes.

    @param sequence_name The name of the option in 'grafon' section that 
      contains a list of classes' names.
    @type sequence_name: str
    @param options_section_name The name of the section that contains options
      for the classes from the sequence.
    @type options_section_name: str

    @return options_dict a dictionary with options for each class.
    @rtype options_dict: dict(str: dict(str: str))
    @return classes_names a sequence of classes that has been read.
    @rtype classes_names: list(str)
    """
    classes_names = \
      self.__config.get(CFG_SEC_GRAFON, sequence_name).split()
    classes_names_mapping = dict(
        zip([name.lower() for name in classes_names], classes_names))
    options_dict = {}
    # Make sure that all classes have options even if as an empty dictionary
    for class_name in classes_names:
      options_dict[class_name] = {}
    # Fill it in 
    for class_option in self.__config.options(options_section_name):
      match = re.match('^([a-zA-Z])*(?=_)', class_option)
      if match:
        match = match.group()
        if match and match in classes_names_mapping:
          value_name = re.sub('^([a-zA-Z])*_', '', class_option)
          options_dict[classes_names_mapping[match]][value_name] = \
            self.__config.get(options_section_name, class_option)
    return options_dict, classes_names
   
  def _get_log_level(self, level_name):
    """!
    Return the numerical value of the level's name.
    
    @param level_name the available log level read from the configuration file.
    @type level_name: str
    """
    level = 0
    if level_name == 'CRITICAL':
      level = logging.CRITICAL
    elif level_name == 'ERROR':
      level = logging.ERROR
    elif level_name == 'WARNING':
      level = logging.WARNING
    elif level_name == 'INFO':
      level = logging.INFO
    elif level_name == 'DEBUG':
      level = logging.DEBUG
    return level


