'''*****************************************************************************
AToMPM - A Tool for Multi-Paradigm Modelling

Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca)

This file is part of AToMPM.

AToMPM is free software: you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later 
version.

AToMPM 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along
with AToMPM.  If not, see <http://www.gnu.org/licenses/>.
*****************************************************************************'''

import re, json, uuid, itertools
from dcal import DesignerCodeAbstractionLayer
from utils import Utilities as utils
from tconstants import TConstants as TC
from pytcore.core.himesis import Himesis, HConstants as HC
from pytcore.core.himesis import HimesisPreConditionPatternLHS
from pytcore.core.himesis import HimesisPreConditionPatternNAC
from pytcore.core.himesis import HimesisPostConditionPattern


''' 
    implements an atompm-to-himesis model compiler and an 
    atompm-to-himesis rule compiler 
    
    _username            the user's username, needed to access files from user's
                              folder
    _aswid                the user's backend's aswid, needed to support httpreqs to
                            backend from rule code
    _defaultDCL            the user's preferred designer code language                        
    _subtypes            a mapping from metamodel types to their subtypes
    _connectorTypes    a set of known 'connector' fulltypes
    _knownMMs            a set of known metamodels (i.e., metamodels for which we've
                            computed subtype and connectorType information)
    _loadedMMs            a set of currently loaded (on the asworker) metamodels 
    _mmTypeData            a mapping of fulltypes to default attributes
    _dcal                     an instance of the DesignerCodeAbstractionLayer
    _compiledRules        a mapping between rule filenames and compiled data 

    *runtime configuration flags*
    RC__looseSubtypingMM        (see _computeSubtypes() for details) '''
class ModelAndRuleCompiler :
    def __init__(self,username,aswid,defaultDCL,mtwid) :
        self._username         = username
        self._aswid                 = aswid
        self._defaultDCL        = defaultDCL
        self._subtypes         = {}        
        self._connectorTypes    = set()
        self._knownMMs            = set()
        self._loadedMMs        = set()
        self._mmTypeData        = {}
        self._mtwid            = mtwid;
        self._dcal                  = DesignerCodeAbstractionLayer(username,aswid,mtwid)
        self._compiledRules  = {}

        self.RC__looseSubtypingMM = None

    
    '''
        add a node to himesis graph 'hg' that reflects the contents of asworker
        node 'n' with id 'id' '''
    def addNode(self,hg,n,id) :
        newNodeIndex = \
                 hg.add_node(n['$type'],n['$type'] in self._connectorTypes)
        hg.vs[newNodeIndex]['$atompmId'] = str(id)
        for attr in n :
            attr = str(attr)
            if attr.startswith('$') :
                hg.vs[newNodeIndex][attr] = n[attr]
            elif attr.startswith('____'):
                hg.vs[newNodeIndex][attr[2:]] = n[attr]['value']
            elif 'value' in n[attr]:
                hg.vs[newNodeIndex][attr] = n[attr]['value']
        return newNodeIndex

    
    

    ''' 
        compile an atompm json model into a pytcore himesis graph 

        0. compute and store the relevant metamodels' subtypes and connector 
            types if we haven't already done so
        1. initialise a Himesis/HimesisPreConditionPatternLHS/... graph 'hg'
        2. in the special case where compileModel() is called as a result of a 
            user loading a model (i.e., from ptcal.loadModel(), detectable because
            himesisBaseClass==Himesis), we also (re)set self._loadedMMs to match 
            the given model's loaded metamodels
        3. if input model is empty, return 'hg'
        4. for each node in input model, create appropriate node in 'hg'
        5. for each edge in input model, create appropriate edge in 'hg'
        6. return 'hg' 

        NOTE: igraph.add_vertices() crashes when attribute names are unicode 
                strings (i.e., u'...')... this is why we convert attr names to
                ascii via 'attr = str(attr)' '''
    def compileModel(self,m,mmsData=None,name=None,himesisBaseClass=Himesis) :
        if m.__class__ != {}.__class__ :
            m = json.loads(m)

        if mmsData != None :
            if mmsData.__class__ != {}.__class__ :
                mmsData = json.loads(mmsData)
            for mm in mmsData :
                if mm not in self._knownMMs :
                    self.parseMetamodel(mm,mmsData[mm])
        else :
            for mm in m['metamodels'] :
                if mm not in self._knownMMs :
                    mmData = utils.fread(
                                            '/users/%s/%s.metamodel'%(self._username,mm))
                    self.parseMetamodel(mm,mmData)
    
        hg = himesisBaseClass(name)
        hg[HC.METAMODELS] = set(m['metamodels'])
        hg[HC.MISSING_METAMODELS] = \
            lambda : hg[HC.METAMODELS] - self._loadedMMs
        hg[HC.GUID] = uuid.uuid4()
        
        if himesisBaseClass == Himesis :
            self._loadedMMs = hg[HC.METAMODELS].copy()
    
        if len(m['nodes']) == 0 :
            return hg

        atompmIds2himesisIndices = {}
        for id in m['nodes'] :
            atompmIds2himesisIndices[id] = self.addNode(hg,m['nodes'][id],id)

        for e in m['edges'] :
            hg.add_edges(
                    [(atompmIds2himesisIndices[str(e['src'])],
                      atompmIds2himesisIndices[str(e['dest'])])])        

        return hg



    '''
        compile one rule (this function defines a number of inner functions, see
        bottom for actual logic) given its atompm model 'r' and its filename 
        'fname'

        1. compile LHS, NACs and RHS into himesis graphs
        2. add remaining properties to compiled patterns (e.g., LHS.NACs) '''
    def compileRule(self,r,fname) :
        if fname in self._compiledRules :
            return self._compiledRules[fname]
        elif r == None :
             r = utils.fread('/users/%s/%s'%(self._username,fname))


        '''
            compile a pattern (e.g. LHS) into a himesis graph

            1. retrieve atompm model(s) of pattern(s) contents
            2. validate __pLabels
            3. foreach pattern>contents pair from step 1
                a) rename metamodels from (.*).pattern to just $1
                b) foreach node in contents
                    i.   rename type from (.*).pattern/__p(.*) to $1/$2
                    ii.  rename __pLabel to HC.MT_LABEL (and delete __pLabel attr)
                    iii. rename __pMatchSubtypes to HC.MT_SUBTYPE_MATCH, if any (and
                          delete __pMatchSubtypes)
                    (the model is now ready to be fed to compileModel)
                c) compile contents into himesis graph, 'hg'
                d) wrap attribute and pattern action/condition code into python 
                    functions

            NOTE: to complete pattern compilation, certain properties still need to
                    be added (e.g., pointer to NACs in LHS) '''
        def compilePattern(patternType,himesisBaseClass) :
            p2pcm = getPatternContents(patternType)
            ''' hergin :: motif-integration :: modify start '''
            if p2pcm==None or len(p2pcm)==0 or len(p2pcm.keys()) == 0 :
                ''' hergin :: motif-integration :: modify end '''
                return []

            res = validateLabels(p2pcm)
            if '$err' in res :
                return res

            hgs = []
            for p,pcm in p2pcm.iteritems() :
                mms = []
                for mm in pcm['metamodels'] :
                    if re.search('.pattern$',mm):
                        mms.append(mm[:-len('.pattern')])
                    elif re.search('.ramified$',mm):
                        mms.append(mm[:-len('.ramified')])
                pcm['metamodels'] = mms
                    
                for id in pcm['nodes'] :
                    n = pcm['nodes'][id]
                    matches = re.match('(.*)\.pattern/__p(.*)',n['$type']) or re.match('(.*)\.ramified/__p(.*)',n['$type'])
                    n['$type'] = matches.group(1)+'/'+matches.group(2)
                    n[HC.MT_LABEL] = n['__pLabel']['value']
                    del n['__pLabel']
                    ''' hergin :: motif-integration start '''
                    if '__pPivotIn' in n:
                        if n['__pPivotIn']['value']:
                            n[HC.MT_PIVOT_IN] = n['__pPivotIn']['value']
                        del n['__pPivotIn']
                    if '__pPivotOut' in n:
                        if n['__pPivotOut']['value']:
                            n[HC.MT_PIVOT_OUT] = n['__pPivotOut']['value']
                        del n['__pPivotOut']
                    ''' hergin :: motif-integration end '''
                    if 'value' in n['__pMatchSubtypes'] :
                        n[HC.MT_SUBTYPE_MATCH] = n['__pMatchSubtypes']['value']
                    del n['__pMatchSubtypes']

                hg = self.compileModel(
                                pcm,
                                name=fname+'_'+patternType,
                                himesisBaseClass=himesisBaseClass)

                if patternType == 'LHS' or patternType == 'NAC' :
                    wrapAttributeDesignerCode(hg,'attrCondition')
                    hg[HC.MT_CONSTRAINT] = r['nodes'][p]['Condition']['value']
                    wrapPatternConditionDesignerCode(hg)
                    for v in hg.vs :
                        v[HC.MT_DIRTY] = False
                        v[HC.MT_SUBTYPES] = (self._subtypes[v[HC.FULLTYPE]] if v[HC.FULLTYPE] in self._subtypes else [])
                elif patternType == 'RHS' :
                    wrapAttributeDesignerCode(hg,'attrAction')
                    hg[HC.MT_ACTION] = r['nodes'][p]['Action']['value']
                    wrapPatternActionDesignerCode(hg)
                elif patternType == 'RHSImport' :
                    def wrapImportedModelAttribute(val) :
                        return lambda arg1,arg2 : val
                    for v in hg.vs :
                        for attr,val in v.attributes().iteritems() :
                            if Himesis.is_RAM_attribute(attr) and val != None :
                                v[attr] = wrapImportedModelAttribute(val)
                    hg[HC.MT_ACTION] = ''
                    wrapPatternActionDesignerCode(hg)    

                hgs.append(hg)            

            return hgs


        ''' 
            read in and slightly alter a model such that it can pass as a 
            traditional RHS's contents... this functions return format is
            this identical to that of getPatternContents(..)

            1. find the RHSImport node
            2. read in the model it refers to
            3. update each of that model's nodes s.t. 
                    a) they have valid __pLabels
                    b) they have empty __matchSubtypes
                    c) they have pattern types
            4. update each of that model's metamodels s.t. that become pattern 
                metamodels '''
        def getImportedModelAsPatternContents() :
            pc = {}
            for id in r['nodes'] :
                if re.search('/RHSImport$',r['nodes'][id]['$type']) :
                    pc[id] = utils.fread(
                        '/users/%s/%s'%(self._username,
                                             r['nodes'][id]['filename']['value']))

                    for nid in pc[id]['nodes'] :
                        n = pc[id]['nodes'][nid]
                        n['__pLabel'] = {'type':'string','value':'$IM_'+str(nid)}
                        n['__pMatchSubtypes'] = {}
                        matches = re.match('(.*)/(.*)',n['$type'])
                        n['$type'] = matches.group(1)+'.pattern/__p'+matches.group(2)
                    pc[id]['metamodels'] = \
                        map(lambda mm : mm+'.pattern', pc[id]['metamodels'])
                    return pc        
                

        ''' 
            return a dict of the form {...,id:contents,...} where 'id' describes a
              node of pattern-type 'pt' (e.g., LHS) and 'contents' is an atompm model
              that contains only 'id''s contents

            0. if 'pt' is not LHS, NAC, RHS, outsource operation 
            1. identify all nodes of type 'pt'
            2. return {} if no matches
            3. map each of step 1's results to every single node that is 
                [transitively] connected to it (except its PatternContents links)...
                  this effectively maps patterns to their contents
            4. based on results from step 3, map each match from step 1 to an 
                atompm model that contains only the pattern's contents
            5. return map from step 4 
            
            NOTE:: the outNeighbors() and getConnectedNodes() inner functions are
                     translated from identical javascript functions in 
                     mmmk.compileToIconDefinitionMetamodel
            '''
        def getPatternContents(pt) :
            if pt == 'RHSImport' :
                return getImportedModelAsPatternContents()

            def outNeighbors(source) :
                return map(lambda x: str(x['dest']),
                              filter(lambda y : y['src'] == source, r['edges']))

            def getConnectedNodes(container,contents) :
                _contents = set()
                for n in outNeighbors(container) :
                    if not n in contents :
                        _contents.add(n)

                if len(_contents) == 0 :
                    return contents
                
                contents = contents | _contents
                return set(utils.flatten(
                            map(lambda x: getConnectedNodes(x,contents),_contents)))

            pc = {}
            for id in r['nodes'] :
                if re.search('/'+pt+'$',r['nodes'][id]['$type']) :
                    pc[id] = []

            if len(pc) == 0 :
                return {}

            for p in pc :
                pc[p] = filter(
                    lambda x: \
                        r['nodes'][x]['$type'] != TC.RULEMM+'/PatternContents', \
                    getConnectedNodes(p,set()))

                m = {'nodes':{},'edges':[],'metamodels':[]}
                mms = []
                for id in pc[p] :
                    m['nodes'][id] = r['nodes'][id]
                    mms.append( utils.getMetamodel(r['nodes'][id]['$type']) )
                m['metamodels'] = list(set(mms))
                m['edges'] = \
                    filter(
                        lambda e : e['src'] in m['nodes'], r['edges'])
                pc[p] = m
                
            return pc         


        ''' 
            ensure none of the nodes specified in the provided list of {patternId:
            patternContentsModel} have empty or duplicate __pLabels... return    error
              if any '''
        def validateLabels(p2pcm) :
            for p,pcm in p2pcm.iteritems() :
                for id in pcm['nodes'] : 
                    if '__pLabel' not in pcm['nodes'][id] :
                        return {'$err':'missing __pLabel attribute'}
                    l = pcm['nodes'][id]['__pLabel']['value']
                    if l == '' :
                        return {'$err':'empty __pLabel'}
                    elif len(
                            filter(
                                lambda x: pcm['nodes'][x]['__pLabel']['value'] == l, 
                                pcm['nodes']) ) > 1 :
                        return {'$err':'duplicate __pLabel :: '+l}
            return {}


        '''
            store a function that evaluates designer-specified javascript attribute
              action/condition code as the value of every RAM attribute (i.e., of 
            every non-Himesis attribute or atompm $ attribute)
        
             NOTE: a little quirk of igraph is that all vertices have all attributes
                    (e.g., v1['a']=5, v2['b']=5 
                         >>  v1:{'a':5,'b':None}, v2:{'a':None,'b':6})... thus, we make
                    sure to only wrap 'real' attributes to avoid 'false' attributes
                      becoming non-None '''
        def wrapAttributeDesignerCode(hg,type) :
            ''' 
                return a python function that will properly execute designer-
                specified javascript action/condition code '''
            def wrap(code,pLabel,attr) :
                def evalAttrCode(pLabel2graphIndexMap,graph):
                    if code == '' :
                        if type == 'attrCondition':
                            return True
                        else :
                            return
                    ex = {}

                    try :
                        self._dcal.configure(
                            self._dcal.identifyLanguage(code) or self._defaultDCL,
                            graph,
                            type,
                            pLabel2graphIndexMap,
                            ex,
                            pLabel,
                            attr)
                        return self._dcal.eval(code)
                    except Exception as e :
                     	if '$err' in ex :
                        	raise RuntimeError(ex['$err'])
                        else :
                            raise RuntimeError(\
                                        'unexpected error encountered while evaluating '+
                                        type+' :: '+str(e))
                return evalAttrCode

            for v in hg.vs :
                for attr,code in v.attributes().iteritems() :
                    if Himesis.is_RAM_attribute(attr) and code != None :
                        v[attr] = wrap(code,v[HC.MT_LABEL],attr)


        '''
            store a function that evaluates designer-specified javascript pattern
              action code as the value of pattern[MT_ACTION] '''
        def wrapPatternActionDesignerCode(hg) :
            def wrap(code) :
                def evalPatternCode(pLabel2graphIndexMap,graph):
                    if code == '' :
                        return []
                    journal = []
                    ex = {}

                    try :
                        self._dcal.configure(
                            self._dcal.identifyLanguage(code) or self._defaultDCL,
                            graph,
                            'patternAction',
                            pLabel2graphIndexMap,
                            ex,
                            journal=journal)
                        self._dcal.eval(code)
                        return journal
                    except Exception as e :
                        if '$err' in ex :
                            raise RuntimeError(ex['$err'])
                        else :
                            raise RuntimeError(\
                                        'unexpected error encountered while evaluating '+
                                        'pattern action code :: '+str(e))
                return evalPatternCode

            hg[HC.MT_ACTION] = wrap(hg[HC.MT_ACTION])


        '''
            store a function that evaluates designer-specified javascript pattern
              condition code as the value of pattern[MT_CONSTRAINT] '''
        def wrapPatternConditionDesignerCode(hg) :
            def wrap(code) :
                def evalPatternCode(pLabel2graphIndexMap,graph) :
                    if code == '' :
                        return True
                    ex = {}

                    try :
                        self._dcal.configure(
                            self._dcal.identifyLanguage(code) or self._defaultDCL,
                            graph,
                            'patternCondition',
                            pLabel2graphIndexMap,
                            ex)
                        return self._dcal.eval(code)
                    except Exception as e :
                        if '$err' in ex :
                            raise RuntimeError(ex['$err'])
                        else :
                            raise RuntimeError(\
                                        'unexpected error encountered while evaluating '+
                                        'pattern condition code :: '+str(e))
                return evalPatternCode

            hg[HC.MT_CONSTRAINT] = wrap(hg[HC.MT_CONSTRAINT])



        lhs  = compilePattern('LHS',HimesisPreConditionPatternLHS)
        if lhs.__class__ == {}.__class__ :
            raise ValueError(fname+' LHS compilation failed on :: '+lhs['$err'])

        nacs = compilePattern('NAC',HimesisPreConditionPatternNAC)
        if nacs.__class__ == {}.__class__ :
            raise ValueError(fname+' NAC compilation failed on :: '+nacs['$err'])

        rhs  = compilePattern('RHS',HimesisPostConditionPattern) or \
                 compilePattern('RHSImport',HimesisPostConditionPattern)
        if rhs.__class__ == {}.__class__ :
            raise ValueError(fname+' RHS compilation failed on :: '+rhs['$err'])

        #lhs[0].NACs = nacs
        for nac in nacs :
            nac.LHS = lhs[0]
            nac.bridge = nac.compute_bridge()
            
        #lhs[0].NACs = nacs
        
        lhs[0].addNACs(nacs)
        
        ''' hergin :: motif-integration start '''
        ''' check condition for RHS for query rule '''
        if len(rhs)>0:
            rhs[0].pre = lhs[0]
            if lhs[0].vcount() > 0 :
                rhs[0].pre_labels = lhs[0].vs[HC.MT_LABEL]
            else :
                rhs[0].pre_labels = []
        
            self._compiledRules[fname] = {'lhs':lhs[0],'rhs':rhs[0]}
        else:
            self._compiledRules[fname] = {'lhs':lhs[0]}
        ''' hergin :: motif-integration end '''
        return self._compiledRules[fname]
    


    '''
        remember the types stored in the 'connectorTypes' property of the passed
          metamodel '''
    def _computeConnectorTypes(self,mm,mmData) :
        for ct in mmData['connectorTypes'].keys() :
            self._connectorTypes.add(mm+'/'+ct)


    '''
        remember the information required to initialize nodes from any of mm's
        types to their default values '''
    def _computeMMTypeData(self,mm,mmData) :
        for type in mmData['types'] :
            fulltype = mm+'/'+type
            self._mmTypeData[fulltype] = {}
            for attr in mmData['types'][type] :
                self._mmTypeData[fulltype][attr['name']] = attr['default']                


    '''
        using the 'types2parentTypes' property of the passed metamodel, construct
        and save a mapping of metamodel types to their subtypes 
        
        NOTE:: to EASE SEMANTICS SHARING (paramount when working with semantic
                   templates), we introduce the 'RC__looseSubtypingMM' compiler 
                 flag... it is used to allow mapToBaseFormalism semantics to be
                 seamlessly used by "looseSubtyping" DSLs... see example below:
                    1 mapToBaseFormalism rule:
                        match BasicState subtypes, produce BasicState
                    2 base formalism subtypes:
                        SimpleStateChart/BasicState subtypes = []
                    3 loose subtyping formalism subtypes:
                        MyDSL/BasicState subtypes = [AAA, BBB]
                    4 result
                        SimpleStateChart/BasicState subtypes = 
                            [MyDSL/BasicState, MyDSL/AAA, MyDSL/BBB]
                        >> rule can now match entities from MyDSL '''
    def _computeSubtypes(self,mm,mmData) :
        t2pt     = mmData['types2parentTypes']
        types    = t2pt.keys()
        parents  = set(itertools.chain.from_iterable(t2pt.values()))
        children = filter(lambda t: t2pt[t] != [], types)
        for type in types :
            fulltype = mm+'/'+type
            if fulltype not in self._subtypes :
                self._subtypes[fulltype] = []
            if type in parents :
                for c in children :
                    if type in t2pt[c] : 
                        self._subtypes[fulltype].append(mm+'/'+c)
            if self.RC__looseSubtypingMM and \
                self.RC__looseSubtypingMM+'/'+type in self._subtypes :
                self._subtypes[fulltype].append(self.RC__looseSubtypingMM+'/'+type)
                self._subtypes[fulltype].extend(
                    self._subtypes[self.RC__looseSubtypingMM+'/'+type])

    
    '''
        forget all compiled rules '''
    def forgetCompiledRules(self) :
        self._compiledRules = {}

    

    '''
          return a reference to self._mmTypeData '''
    def getMMTypeData(self) :
        return self._mmTypeData



    '''
        compute and store the specified metamodel's default attributes, subtypes
          and connector types 

        1. if we already know 'mm' (e.g., user may be re-loading it or a newer 
            version of it),
            a) clear known subtypes for that 'mm' in-place... we do this clearing
                in-place (i.e., del L[:] vs. L = []) because compiled pattern nodes
                  have pointers to entries in self._subtypes... this implies that to
                  avoid having to recompile rules when we alter self._subtypes, 
                alterations made to existing entries need to preserve pointers
            b) clear known connector types for that 'mm'
        2. do the deed
        3. if 'loadMM' is specified (i.e., this function is called as a result of
            a user LOADMM changelog, not as a result of encountering an unknown 
            metamodel while compiling a rule '''
    def parseMetamodel(self,mm,mmData,loadMM=False) :
        if mm in self._knownMMs :
            for type in mmData['types'] :
                fulltype = mm+'/'+type
                if fulltype in self._subtypes :
                    self._subtypes[fulltype][:] = []
                if fulltype in self._connectorTypes :
                    self._connectorTypes.remove(fulltype)

        self._computeSubtypes(mm,mmData)
        self._computeConnectorTypes(mm,mmData)
        self._computeMMTypeData(mm,mmData)
        self._knownMMs.add(mm)
        if loadMM :
            self._loadedMMs.add(mm)

                

    '''
        remove a metamodel from the list of currently loaded (on the asworker) 
        metamodels '''
    def unloadMetamodel(self,mm):
        if mm in self._loadedMMs:
            self._loadedMMs.remove(mm)    
