# -*- coding: utf-8 -*-
#
# Markov Logic Networks
#
# (C) 2012-2013 by Daniel Nyga (nyga@cs.uni-bremen.de)
# (C) 2006-2011 by Dominik Jain (jain@cs.tum.edu)
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import pyparsing
from dnutils import logs, ifnone, out

from ..logic import FirstOrderLogic, FuzzyLogic

import platform
from .mrf import MRF
from .errors import MLNParsingError
from pyparsing import ParseException
from .constants import HARD, comment_color, predicate_color, weight_color
import copy
import os
from .util import StopWatch, mergedom, fstr, colorize, stripComments
from .mlnpreds import (Predicate, FuzzyPredicate, SoftFunctionalPredicate,
    FunctionalPredicate)
from .database import Database
from .learning.multidb import MultipleDatabaseLearner
import sys
import re
import traceback
from .learning.bpll import BPLL
from ..utils.project import mlnpath
from importlib import util as imputil

logger = logs.getlogger(__name__)


if platform.architecture()[0] == '32bit':
    if imputil.find_spec('psyco') is not None:
        import psyco # Don't use Psyco when debugging! @UnresolvedImport
        psyco.full()
    else:
        logger.warning("Note: Psyco (http://psyco.sourceforge.net) was not loaded. On 32bit systems, it is recommended to install it for improved performance.")


class MLN(object):
    '''
    Represents a Markov logic network.
    
    :member formulas:    a list of :class:`logic.common.Formula` objects representing the formulas of the MLN.
    :member predicates:  a dict mapping predicate names to :class:`mlnpreds.Predicate` objects.
    
    :param logic:        (string) the type of logic to be used in this MLN. Possible values
                         are `FirstOrderLogic` and `FuzzyLogic`.
    :param grammar:      (string) the syntax to be used. Possible grammars are
                         `PRACGrammar` and `StandardGrammar`.
    :param mlnfile:      can be a path to an MLN file or a file object.
    '''


    def __init__(self, logic='FirstOrderLogic', grammar='PRACGrammar', mlnfile=None):
        # instantiate the logic and grammar
        logic_str = '%s("%s", self)' % (logic, grammar)
        self.logic = eval(logic_str)
        logger.debug('Creating MLN with %s syntax and %s semantics' % (grammar, logic))
        self._predicates = {} # maps from predicate name to the predicate instance
        self.domains = {}    # maps from domain names to list of values
        self._formulas = []   # list of MLNFormula instances
        self.domain_decls = []
        self.weights = []
        self.fixweights = []
        self.vars = {}
        self._unique_templvars = []
        self._probreqs = []
        self._materialized = False
        self.fuzzypreds = []  # for saving fuzzy predicates that have been converted to binary preds
        if mlnfile is not None:
            MLN.load(mlnfile, logic=logic, grammar=grammar, mln=self)
            return
        self.closedWorldPreds = []
        self.formulaGroups = []
        self.templateIdx2GroupIdx = {}
        self.posteriorProbReqs = []
        self.watch = StopWatch()

    @property
    def predicates(self):
        return list(self.iterpreds())
     
    @property
    def formulas(self):
        return list(self._formulas)
        
    @property
    def weights(self):
        return self._weights
    
    @weights.setter
    def weights(self, wts):
        if len(wts) != len(self._formulas):
            raise Exception('Weight vector must have the same length as formula vector.')
            wts = map(lambda w: float('%-10.6f' % float(eval(str(w)))) if type(w) in (float, int) and w not in (
            HARD, float('inf'), -float('inf')) else w, wts)
        self._weights = wts

    @property
    def fixweights(self):
        return self._fixweights

    @fixweights.setter
    def fixweights(self, fw):
        self._fixweights = fw

    @property
    def probreqs(self):
        return self._probreqs
    
    @property
    def weighted_formulas(self):
        return [f for f in self._formulas if f.weight is not HARD]

    @property
    def prednames(self):
        return [p.name for p in self.predicates]

    def prior(self, f, p):
        self._probreqs.append(FirstOrderLogic.PriorConstraint(formula=f, p=p))

    def posterior(self, f, p):
        self._probreqs.append(FirstOrderLogic.PosteriorConstraint(formula=f, p=p))

    def copy(self):
        '''
        Returns a deep copy of this MLN, which is not yet materialized.
        '''
        mln_ = MLN(logic=self.logic.__class__.__name__, grammar=self.logic.grammar.__class__.__name__)
        for pred in self.iterpreds():
            mln_.predicate(copy.copy(pred))
        mln_.domain_decls = list(self.domain_decls)
        for i, f in self.iterformulas():
            mln_.formula(f.copy(mln=mln_), weight=self.weight(i), fixweight=self.fixweights[i], unique_templvars=self._unique_templvars[i])
        mln_.domains = dict(self.domains)
        mln_.vars = dict(self.vars)
        mln_._probreqs = list(self.probreqs)
        mln_.fuzzypreds = list(self.fuzzypreds)
        return mln_

    def predicate(self, predicate):
        '''
        Returns the predicate object with the given predicate name, or declares a new predicate.
        
        If predicate is a string, this method returns the predicate object 
        assiciated to the given predicate name. If it is a predicate instance, it declares the
        new predicate in this MLN and returns the MLN instance. In the latter case, this is 
        equivalent to `MLN.declare_predicate()`.
        
        :param predicate:    name of the predicate to be returned or a `Predicate` instance
                             specifying the predicate to be declared.
        :returns:            the Predicate object or None if there is no predicate with this name.
                             If a new predicate is declared, returns this MLN instance.
                             
        :Example:
        
        >>> mln = MLN()
        >>> mln.predicate(Predicate(foo, [arg0, arg1]))
               .predicate(Predicate(bar, [arg1, arg2])) # this declares predicates foo and bar
        >>> mln.predicate('foo')
        <Predicate: foo(arg0,arg1)>
        
        '''
        if isinstance(predicate, Predicate):
            return self.declare_predicate(predicate)
        elif isinstance(predicate, str):
            return self._predicates.get(predicate, None)
        elif isinstance(predicate, pyparsing.ParseResults):
            return predicate.asList()
        else:
            raise Exception('Illegal type of argument predicate: %s' % type(predicate))
        
    def iterpreds(self):
        '''
        Yields the predicates defined in this MLN alphabetically ordered.
        '''
        for predname in sorted(self._predicates):
            yield self.predicate(predname)
    
    def update_predicates(self, mln):
        '''
        Merges the predicate definitions of this MLN with the definitions
        of the given one.
        
        :param mln:     an instance of an MLN object.
        '''
        for pred in mln.iterpreds():
            self.declare_predicate(pred)
    
    def declare_predicate(self, predicate):
        '''
        Adds a predicate declaration to the MLN:
        
        :param predicate:      an instance of a Predicate or one of its subclasses 
                               specifying a predicate declaration.
        '''
        pred = self._predicates.get(predicate.name)
        if pred is not None and  pred != predicate:
            raise Exception('Contradictory predicate definitions: %s <--> %s' % (pred, predicate))
        else:
            self._predicates[predicate.name] = predicate
            for dom in predicate.argdoms:
                if dom not in self.domains:
                    self.domains[dom] = []
        return self

    def formula(self, formula, weight=0., fixweight=False, unique_templvars=None):
        '''
        Adds a formula to this MLN. The respective domains of constants
        are updated, if necessary. If `formula` is an integer, returns the formula
        with the respective index or the formula object that has been created from
        formula. The formula will be automatically tied to this MLN.
        
        :param formula:             a `Logic.Formula` object or a formula string
        :param weight:              an optional weight. May be a mathematical expression
                                    as a string (e.g. log(0.1)), real-valued number
                                    or `mln.infty` to indicate a hard formula.
        :param fixweight:           indicates whether or not the weight of this
                                    formula should be fixed during learning.
        :param unique_templvars:    specifies a list of template variables that will create
                                    only unique combinations of expanded formulas
        '''
        if type(formula) is str:
            formula = self.logic.parse_formula(formula)
        elif type(formula) is int:
            return self._formulas[formula]
        constants = {}
        formula.vardoms(None, constants)
        for domain, constants in constants.items():
            for c in constants: self.constant(domain, c)
        formula.mln = self
        formula.idx = len(self._formulas)
        self._formulas.append(formula)
        self.weights.append(weight)
        self.fixweights.append(fixweight)
        self._unique_templvars.append(list(unique_templvars) if unique_templvars is not None else [])
        return self._formulas[-1]

    def _rmformulas(self):