"""!
@package gmodeler.model

@brief wxGUI Graphical Modeler (base classes & read/write)

Classes:
 - model::Model
 - model::ModelObject
 - model::ModelAction
 - model::ModelData
 - model::ModelRelation
 - model::ModelItem
 - model::ModelLoop
 - model::ModelCondition
 - model::ProcessModelFile
 - model::WriteModelFile
 - model::WritePythonFile
 - model::ModelParamDialog

(C) 2010-2012 by the GRASS Development Team

This program is free software under the GNU General Public License
(>=v2). Read the file COPYING that comes with GRASS for details.

@author Martin Landa <landa.martin gmail.com>
"""

import os
import getpass
import copy
import re
import mimetypes
import time
try:
    import xml.etree.ElementTree as etree
except ImportError:
    import elementtree.ElementTree as etree # Python <= 2.4

import wx
from wx.lib import ogl

from core                import globalvar
from core                import utils
from core.gcmd           import GMessage, GException, GError, RunCommand, EncodeString, GWarning, GetDefaultEncoding
from core.settings       import UserSettings
from gui_core.forms      import GUI, CmdPanel
from gui_core.widgets    import GNotebook

from grass.script import core as grass
from grass.script import task as gtask

class Model(object):
    """!Class representing the model"""
    def __init__(self, canvas = None):
        self.items      = list() # list of actions/loops/...
        
        # model properties
        self.properties = { 'name'        : _("model"),
                            'description' : _("Script generated by wxGUI Graphical Modeler."),
                            'author'      : getpass.getuser() }
        # model variables
        self.variables = dict()
        self.variablesParams = dict()
        
        self.canvas  = canvas
        
    def GetCanvas(self):
        """!Get canvas or None"""
        return self.canvas
    
    def GetItems(self, objType = None):
        """!Get list of model items

        @param objType Object type to filter model objects
        """
        if not objType:
            return self.items
        
        result = list()
        for item in self.items:
            if isinstance(item, objType):
                result.append(item)
        
        return result

    def GetItem(self, aId):
        """!Get item of given id

        @param aId item id
        
        @return Model* instance
        @return None if no item found
        """
        ilist = self.GetItems()
        for item in ilist:
            if item.GetId() == aId:
                return item
        
        return None

    def GetNumItems(self, actionOnly = False):
        """!Get number of items"""
        if actionOnly:
            return len(self.GetItems(objType = ModelAction))
        
        return len(self.GetItems())

    def GetNextId(self):
        """!Get next id (data ignored)

        @return next id to be used (default: 1)
        """
        if len(self.items) < 1:
            return 1
        
        currId = self.items[-1].GetId()
        if currId > 0:
            return currId + 1
        
        return 1

    def GetProperties(self):
        """!Get model properties"""
        return self.properties

    def GetVariables(self, params = False):
        """!Get model variables"""
        if params:
            return self.variablesParams
        
        return self.variables
    
    def SetVariables(self, data):
        """!Set model variables"""
        self.variables = data
    
    def Reset(self):
        """!Reset model"""
        self.items = list()
        
    def RemoveItem(self, item):
        """!Remove item from model
        
        @return list of related items to remove/update
        """
        relList = list()
        upList = list()
        
        if not isinstance(item, ModelData):
            self.items.remove(item)
        
        if isinstance(item, ModelAction):
            for rel in item.GetRelations():
                relList.append(rel)
                data = rel.GetData()
                if len(data.GetRelations()) < 2:
                    relList.append(data)
                else:
                    upList.append(data)
            
        elif isinstance(item, ModelData):
            for rel in item.GetRelations():
                relList.append(rel)
                if rel.GetFrom() == self:
                    relList.append(rel.GetTo())
                else:
                    relList.append(rel.GetFrom())
        
        elif isinstance(item, ModelLoop):
            for rel in item.GetRelations():
                relList.append(rel)
            for action in self.GetItems():
                action.UnSetBlock(item)
        
        return relList, upList
    
    def FindAction(self, aId):
        """!Find action by id"""
        alist = self.GetItems(objType = ModelAction)
        for action in alist:
            if action.GetId() == aId:
                return action
        
        return None

    def GetMaps(self, prompt):
        """!Get list of maps of selected type

        @param promt to filter maps"""
        maps = list()
        for data in self.GetData():
            if prompt == data.GetPrompt():
                maps.append(data.GetValue())
        return maps

    def GetData(self):
        """!Get list of data items"""
        result = list()
        dataItems = self.GetItems(objType = ModelData)
        
        for action in self.GetItems(objType = ModelAction):
            for rel in action.GetRelations():
                dataItem = rel.GetData()
                if dataItem not in result:
                    result.append(dataItem)
                if dataItem in dataItems:
                    dataItems.remove(dataItem)
        
        # standalone data
        if dataItems:
            result += dataItems
        
        return result

    def FindData(self, value, prompt):
        """!Find data item in the model

        @param value value
        @param prompt prompt

        @return ModelData instance
        @return None if not found
        """
        for data in self.GetData():
            if data.GetValue() == value and \
                    data.GetPrompt() == prompt:
                return data
        
        return None
                
    def LoadModel(self, filename):
        """!Load model definition stored in GRASS Model XML file (gxm)
        
        @todo Validate against DTD
        
        Raise exception on error.
        """
        dtdFilename = os.path.join(globalvar.ETCWXDIR, "xml", "grass-gxm.dtd")
        
        # parse workspace file
        try:
            gxmXml = ProcessModelFile(etree.parse(filename))
        except StandardError, e:
            raise GException(e)
        
        if self.canvas:
            win = self.canvas.parent
            if gxmXml.pos:
                win.SetPosition(gxmXml.pos)
            if gxmXml.size:
                win.SetSize(gxmXml.size)
        
        # load properties
        self.properties = gxmXml.properties
        self.variables  = gxmXml.variables
        
        # load model.GetActions()
        for action in gxmXml.actions:
            actionItem = ModelAction(parent = self, 
                                     x = action['pos'][0],
                                     y = action['pos'][1],
                                     width = action['size'][0],
                                     height = action['size'][1],
                                     task = action['task'],
                                     id = action['id'])
            
            if action['disabled']:
                actionItem.Enable(False)
            
            self.AddItem(actionItem)
            
            actionItem.SetValid(actionItem.GetTask().get_options())
            actionItem.GetLog() # substitute variables (-> valid/invalid)
        
        # load data & relations
        for data in gxmXml.data:
            dataItem = ModelData(parent = self, 
                                 x = data['pos'][0],
                                 y = data['pos'][1],
                                 width = data['size'][0],
                                 height = data['size'][1],
                                 prompt = data['prompt'],
                                 value = data['value'])
            dataItem.SetIntermediate(data['intermediate'])
            
            for rel in data['rels']:
                actionItem = self.FindAction(rel['id'])
                if rel['dir'] == 'from':
                    relation = ModelRelation(parent = self, fromShape = dataItem,
                                             toShape = actionItem, param = rel['name'])
                else:
                    relation = ModelRelation(parent = self, fromShape = actionItem,
                                             toShape = dataItem, param = rel['name'])
                relation.SetControlPoints(rel['points'])
                actionItem.AddRelation(relation)
                dataItem.AddRelation(relation)

            if self.canvas:
                dataItem.Update()
           
        # load loops
        for loop in gxmXml.loops:
            loopItem = ModelLoop(parent = self, 
                                 x = loop['pos'][0],
                                 y = loop['pos'][1],
                                 width = loop['size'][0],
                                 height = loop['size'][1],
                                 text = loop['text'],
                                 id = loop['id'])
            self.AddItem(loopItem)

        # load conditions
        for condition in gxmXml.conditions:
            conditionItem = ModelCondition(parent = self, 
                                           x = condition['pos'][0],
                                           y = condition['pos'][1],
                                           width = condition['size'][0],
                                           height = condition['size'][1],
                                           text = condition['text'],
                                           id = condition['id'])
            self.AddItem(conditionItem)

        # define loops & if/else items
        for loop in gxmXml.loops:
            alist = list()
            for aId in loop['items']:
                action = self.GetItem(aId)
                alist.append(action)
            
            loopItem = self.GetItem(loop['id'])
            loopItem.SetItems(alist)
            
            for action in loopItem.GetItems():
                action.SetBlock(loopItem)
        
        for condition in gxmXml.conditions:
            conditionItem = self.GetItem(condition['id'])
            for b in condition['items'].keys():
                alist = list()
                for aId in condition['items'][b]:
                    action = self.GetItem(aId)
                    alist.append(action)
                conditionItem.SetItems(alist, branch = b)
            
            items = conditionItem.GetItems()
            for b in items.keys():
                for action in items[b]:
                    action.SetBlock(conditionItem)
        
    def AddItem(self, newItem):
        """!Add item to the list"""
        iId = newItem.GetId()
        
        i  = 0
        for item in self.items:
            if item.GetId() > iId:
                self.items.insert(i, newItem)
                return
            i += 1
        
        self.items.append(newItem)
        
    def IsValid(self):
        """Return True if model is valid"""
        if self.Validate():
            return False
        
        return True
    
    def Validate(self):
        """!Validate model, return None if model is valid otherwise
        error string"""
        errList = list()

        variables = self.GetVariables().keys()
        pattern = re.compile(r'(.*)(%.+\s?)(.*)')
        for action in self.GetItems(objType = ModelAction):
            cmd = action.GetLog(string = False)
            
            task = GUI(show = None).ParseCommand(cmd = cmd)
            errList += map(lambda x: cmd[0] + ': ' + x, task.get_cmd_error())
            
            # check also variables
            for opt in cmd[1:]:
                if '=' not in opt:
                    continue
                key, value = opt.split('=', 1)
                sval = pattern.search(value)
                if sval:
                    var = sval.group(2).strip()[1:] # ignore '%'
                    if var not in variables:
                        report = True
                        for item in filter(lambda x: isinstance(x, ModelLoop), action.GetBlock()):
                            if var in item.GetText():
                                report = False
                                break
                        if report:
                            errList.append(cmd[0] + ": " + _("undefined variable '%s'") % var)
            ### TODO: check variables in file only optionally
            ### errList += self._substituteFile(action, checkOnly = True)
        
        return errList

    def _substituteFile(self, item, params = None, checkOnly = False):
        """!Subsitute variables in command file inputs

        @param checkOnly tuble - True to check variable, don't touch files
        
        @return list of undefined variables
        """
        errList = list()
        
        self.fileInput = dict()
        
        # collect ascii inputs
        for p in item.GetParams()['params']:
            if p.get('element', '') == 'file' and \
                    p.get('prompt', '') == 'input' and \
                    p.get('age', '') == 'old':
                filename = p.get('value', p.get('default', ''))
                if filename and \
                        mimetypes.guess_type(filename)[0] == 'text/plain':
                    self.fileInput[filename] = None
        
        for finput in self.fileInput:
            # read lines
            fd = open(finput, "r")
            try:
                data = self.fileInput[finput] = fd.read()
            finally:
                fd.close()
            
            # substitute variables
            write = False
            variables = self.GetVariables()
            for variable in variables:
                pattern = re.compile('%' + variable)
                value = ''
                if params and 'variables' in params:
                    for p in params['variables']['params']:
                        if variable == p.get('name', ''):
                            if p.get('type', 'string') == 'string':
                                value = p.get('value', '')
                            else:
                                value = str(p.get('value', ''))
                            break
                
                if not value:
                    value = variables[variable].get('value', '')
                
                data = pattern.sub(value, data)
                if not checkOnly:
                    write = True
            
            pattern = re.compile(r'(.*)(%.+\s?)(.*)')
            sval = pattern.search(data)
            if sval:
                var = sval.group(2).strip()[1:] # ignore '%'
                cmd = item.GetLog(string = False)[0]
                errList.append(cmd + ": " + _("undefined variable '%s'") % var)
            
            if not checkOnly:
                if write:
                    fd = open(finput, "w")
                    try:
                        fd.write(data)
                    finally:
                        fd.close()
                else:
                    self.fileInput[finput] = None
        
        return errList
    
    def OnPrepare(self, item, params):
        self._substituteFile(item, params, checkOnly = False)

    def RunAction(self, item, params, log, onDone, onPrepare = None, statusbar = None):
        """!Run given action

        @param item action item
        @param params parameters dict
        @param log logging window
        @param onDone on-done method
        @param onPrepare on-prepare method
        @param statusbar wx.StatusBar instance or None
        """
        name = item.GetName()
        if name in params:
            paramsOrig = item.GetParams(dcopy = True)
            item.MergeParams(params[name])
        
        if statusbar:
            statusbar.SetStatusText(_('Running model...'), 0)
            
        data = { 'item' : item,
                 'params' : copy.deepcopy(params) }
        log.RunCmd(command = item.GetLog(string = False, substitute = params),
                   onDone = onDone, onPrepare = self.OnPrepare, userData = data)
        
        if name in params:
            item.SetParams(paramsOrig)

    def Run(self, log, onDone, parent = None):
        """!Run model

        @param log logging window (see gconsole.GConsole)
        @param onDone on-done method
        @param parent window for messages or None
        """
        if self.GetNumItems() < 1:
            GMessage(parent = parent,
                     message = _('Model is empty. Nothing to run.'))
            return
        
        statusbar = None
        if isinstance(parent, wx.Frame):
            statusbar = parent.GetStatusBar()
        
        # validation
        if statusbar:
            statusbar.SetStatusText(_('Validating model...'), 0)
        errList = self.Validate()
        if statusbar:
            statusbar.SetStatusText('', 0)
        if errList:
            dlg = wx.MessageDialog(parent = parent,
                                   message = _('Model is not valid. Do you want to '
                                               'run the model anyway?\n\n%s') % '\n'.join(errList),
                                   caption = _("Run model?"),
                                   style = wx.YES_NO | wx.NO_DEFAULT |
                                   wx.ICON_QUESTION | wx.CENTRE)
            ret = dlg.ShowModal()
            dlg.Destroy()
            if ret != wx.ID_YES:
                return
        
        # parametrization
        params = self.Parameterize()
        delInterData = False
        if params:
            dlg = ModelParamDialog(parent = parent,
                                   params = params)
            dlg.CenterOnParent()
            
            ret = dlg.ShowModal()
            if ret != wx.ID_OK:
                dlg.Destroy()
                return
            
            err = dlg.GetErrors()
            delInterData = dlg.DeleteIntermediateData()
            dlg.Destroy()
            if err:
                GError(parent = parent, message = unicode('\n'.join(err)))
                return
            
            err = list()
            for key, item in params.iteritems():
                for p in item['params']:
                    if p.get('value', '') == '':
                        err.append((key, p.get('name', ''), p.get('description', '')))
            if err:
                GError(parent = parent,
                       message = _("Variables below not defined:") + \
                           "\n\n" + unicode('\n'.join(map(lambda x: "%s: %s (%s)" % (x[0], x[1], x[2]), err))))
                return
        
        log.cmdThread.SetId(-1)
        for item in self.GetItems():
            if not item.IsEnabled():
                continue
            if isinstance(item, ModelAction):
                if item.GetBlockId():
                    continue
                self.RunAction(item, params, log, onDone)
            elif isinstance(item, ModelLoop):
                cond = item.GetText()
                # substitute variables in condition
                variables = self.GetVariables()
                for variable in variables:
                    pattern = re.compile('%' + variable)
                    if pattern.search(cond):
                        value = ''
                        if params and 'variables' in params:
                            for p in params['variables']['params']:
                                if variable == p.get('name', ''):
                                    value = p.get('value', '')
                                    break
                        
                        if not value:
                            value = variables[variable].get('value', '')
                        
                        if not value:
                            continue
                        
                        vtype = variables[variable].get('type', 'string')
                        if vtype == 'string':
                            value = '"' + value + '"'
                        cond = pattern.sub(value, cond)
                
                # split condition
                condVar, condText = map(lambda x: x.strip(), re.split('\s*in\s*', cond))
                pattern = re.compile('%' + condVar)
                ### for vars()[condVar] in eval(condText): ?
                vlist = list()
                if condText[0] == '`' and condText[-1] == '`':
                    # run command
                    cmd, dcmd = utils.CmdToTuple(condText[1:-1].split(' '))
                    ret = RunCommand(cmd,
                                     read = True,
                                     **dcmd)
                    if ret:
                        vlist = ret.splitlines()
                else:
                    vlist = eval(condText)
                
                if 'variables' not in params:
                    params['variables'] = { 'params' : [] }
                varDict = { 'name' : condVar, 'value' : '' }
                params['variables']['params'].append(varDict)
                                
                for var in vlist:
                    for action in item.GetItems():
                        if not isinstance(action, ModelAction) or \
                                not action.IsEnabled():
                            continue
                        
                        varDict['value'] = var
                                                
                        self.RunAction(item = action, params = params,
                                       log = log, onDone = onDone)
                params['variables']['params'].remove(varDict)
        
        if delInterData:
            self.DeleteIntermediateData(log)
        
        # discard values
        if params:
            for item in params.itervalues():
                for p in item['params']:
                    p['value'] = ''
        
    def DeleteIntermediateData(self, log):
        """!Detele intermediate data"""
        rast, vect, rast3d, msg = self.GetIntermediateData()
        
        if rast:
            log.RunCmd(['g.remove', 'rast=%s' %','.join(rast)])
        if rast3d:
            log.RunCmd(['g.remove', 'rast3d=%s' %','.join(rast3d)])
        if vect:
            log.RunCmd(['g.remove', 'vect=%s' %','.join(vect)])
        
    def GetIntermediateData(self):
        """!Get info about intermediate data"""
        rast = list()
        rast3d = list()
        vect = list()
        for data in self.GetData():
            if not data.IsIntermediate():
                continue
            name = data.GetValue()
            prompt = data.GetPrompt()
            if prompt == 'raster':
                rast.append(name)
            elif prompt == 'vector':
                vect.append(name)
            elif prompt == 'rast3d':
                rast3d.append(name)
        
        msg = ''
        if rast:
            msg += '\n\n%s: ' % _('Raster maps')
            msg += ', '.join(rast)
        if rast3d:
            msg += '\n\n%s: ' % _('3D raster maps')
            msg += ', '.join(rast3d)
        if vect:
            msg += '\n\n%s: ' % _('Vector maps')
            msg += ', '.join(vect)
        
        return rast, vect, rast3d, msg

    def Update(self):
        """!Update model"""
        for item in self.items:
            item.Update()
        
    def IsParameterized(self):
        """!Return True if model is parameterized"""
        if self.Parameterize():
            return True
        
        return False
    
    def Parameterize(self):
        """!Return parameterized options"""
        result = dict()
        idx = 0
        if self.variables:
            params = list()
            result["variables"] = { 'flags'  : list(),
                                    'params' : params,
                                    'idx'    : idx }
            for name, values in self.variables.iteritems():
                gtype = values.get('type', 'string')
                if gtype in ('raster', 'vector', 'mapset', 'file'):
                    gisprompt = True
                    prompt = gtype
                    if gtype == 'raster':
                        element = 'cell'
                    else:
                        element = gtype
                    ptype = 'string'
                else:
                    gisprompt = False
                    prompt = None
                    element = None
                    ptype = gtype
                params.append({ 'gisprompt' : gisprompt,
                                'multiple'  : False,
                                'description' : values.get('description', ''),
                                'guidependency' : '',
                                'default' : '',
                                'age' : None,
                                'required' : True,
                                'value' : values.get('value', ''),
                                'label' : '',
                                'guisection' : '',
                                'key_desc' : '',
                                'values' : list(),
                                'parameterized' : False,
                                'values_desc' : list(),
                                'prompt' : prompt,
                                'element' : element,
                                'type' : ptype,
                                'name' : name })
            
            idx += 1
        
        for action in self.GetItems(objType = ModelAction):
            if not action.IsEnabled():
                continue
            name   = action.GetName()
            params = action.GetParams()
            for f in params['flags']:
                if f.get('parameterized', False):
                    if name not in result:
                        result[name] = { 'flags' : list(),
                                         'params': list(),
                                         'idx'   : idx }
                    result[name]['flags'].append(f)
            for p in params['params']:
                if p.get('parameterized', False):
                    if name not in result:
                        result[name] = { 'flags' : list(),
                                         'params': list(),
                                         'idx'   : idx }
                    result[name]['params'].append(p)
            if name in result:
                idx += 1
        
        self.variablesParams = result # record parameters
        
        return result

class ModelObject(object):
    def __init__(self, id = -1):
        self.id   = id
        self.rels = list() # list of ModelRelations
        
        self.isEnabled = True
        self.inBlock   = list() # list of related loops/conditions
        
    def __del__(self):
        pass
    
    def GetId(self):
        """!Get id"""
        return self.id
    
    def AddRelation(self, rel):
        """!Record new relation
        """
        self.rels.append(rel)

    def GetRelations(self, fdir = None):
        """!Get list of relations
        
        @param fdir True for 'from'
        """
        if fdir is None:
            return self.rels
        
        result = list()
        for rel in self.rels:
            if fdir == 'from':
                if rel.GetFrom() == self:
                    result.append(rel)
            else:
                if rel.GetTo() == self:
                    result.append(rel)
        
        return result
    
    def IsEnabled(self):
        """!Get True if action is enabled, otherwise False"""
        return self.isEnabled
    
    def Enable(self, enabled = True):
        """!Enable/disable action"""
        self.isEnabled = enabled
        self.Update()

    def Update(self):
        pass

    def SetBlock(self, item):
        """!Add object to the block (loop/condition)

        @param item reference to ModelLoop or ModelCondition which
        defines loops/condition
        """
        if item not in self.inBlock:
            self.inBlock.append(item)
        
    def UnSetBlock(self, item):
        """!Remove object from the block (loop/consition)

        @param item reference to ModelLoop or ModelCondition which
        defines loops/codition
        """
        if item in self.inBlock:
            self.inBlock.remove(item)
        
    def GetBlock(self):
        """!Get list of related ModelObject(s) which defines block
        (loop/condition)

        @return list of ModelObjects
        """
        return self.inBlock
    
    def GetBlockId(self):
        """!Get list of related ids which defines block

        @return list of ids
        """
        ret = list()
        for mo in self.inBlock:
            ret.append(mo.GetId())
        
        return ret
    
class ModelAction(ModelObject, ogl.RectangleShape):
    """!Action class (GRASS module)"""
    def __init__(self, parent, x, y, id = -1, cmd = None, task = None, width = None, height = None):
        ModelObject.__init__(self, id)
        
        self.parent  = parent
        self.task    = task
        
        if not width:
            width = UserSettings.Get(group='modeler', key='action', subkey=('size', 'width'))
        if not height:
            height = UserSettings.Get(group='modeler', key='action', subkey=('size', 'height'))
        
        if cmd:
            self.task = GUI(show = None).ParseCommand(cmd = cmd)
        else:
            if task:
                self.task = task
            else:
                self.task = None
        
        self.propWin = None
        
        self.data = list()   # list of connected data items
        
        self.isValid = False
        self.isParameterized = False
        
        if self.parent.GetCanvas():
            ogl.RectangleShape.__init__(self, width, height)
            
            self.SetCanvas(self.parent)
            self.SetX(x)
            self.SetY(y)
            self.SetPen(wx.BLACK_PEN)
            self._setPen()
            self._setBrush()
            self.SetId(id)
        
        if self.task:
            self.SetValid(self.task.get_options())
        
    def _setBrush(self, running = False):
        """!Set brush"""
        if running:
            color = UserSettings.Get(group='modeler', key='action',
                                     subkey=('color', 'running'))
        elif not self.isEnabled:
            color = UserSettings.Get(group='modeler', key='disabled',
                                     subkey='color')
        elif self.isValid:
            color = UserSettings.Get(group='modeler', key='action',
                                     subkey=('color', 'valid'))
        else:
            color = UserSettings.Get(group='modeler', key='action',
                                     subkey=('color', 'invalid'))
        
        wxColor = wx.Colour(color[0], color[1], color[2])
        self.SetBrush(wx.Brush(wxColor))
        
    def _setPen(self):
        """!Set pen"""
        if self.isParameterized:
            width = int(UserSettings.Get(group='modeler', key='action',
                                         subkey=('width', 'parameterized')))
        else:
            width = int(UserSettings.Get(group='modeler', key='action',
                                         subkey=('width', 'default')))
        pen = self.GetPen()
        pen.SetWidth(width)
        self.SetPen(pen)

    def SetId(self, id):
        """!Set id"""
        self.id = id
        cmd = self.task.get_cmd(ignoreErrors = True)
        if cmd and len(cmd) > 0:
            self.ClearText()
            self.AddText('(%d) %s' % (self.id, cmd[0]))
        else:
            self.AddText('(%d) <<%s>>' % (self.id, _("unknown")))
        
    def SetProperties(self, params, propwin):
        """!Record properties dialog"""
        self.task.params = params['params']
        self.task.flags  = params['flags']
        self.propWin = propwin

    def GetPropDialog(self):
        """!Get properties dialog"""
        return self.propWin

    def GetLog(self, string = True, substitute = None):
        """!Get logging info

        @param string True to get cmd as a string otherwise a list
        @param substitute dictionary of parameter to substitute or None
        """
        cmd = self.task.get_cmd(ignoreErrors = True, ignoreRequired = True,
                                ignoreDefault = False)
        
        # substitute variables
        if substitute:
            variables = []
            if 'variables' in substitute:
                for p in substitute['variables']['params']:
                    variables.append(p.get('name', ''))
            else:
                variables = self.parent.GetVariables()
            for variable in variables:
                pattern= re.compile('%' + variable)
                value = ''
                if substitute and 'variables' in substitute:
                    for p in substitute['variables']['params']:
                        if variable == p.get('name', ''):
                            if p.get('type', 'string') == 'string':
                                value = p.get('value', '')
                            else:
                                value = str(p.get('value', ''))
                            break
                    
                if not value:
                    value = variables[variable].get('value', '')
                
                if not value:
                    continue
                
                for idx in range(len(cmd)):
                    if pattern.search(cmd[idx]):
                        cmd[idx] = pattern.sub(value, cmd[idx])
                        break
                    idx += 1
        
        if string:
            if cmd is None:
                return ''
            else:
                return ' '.join(cmd)
        
        return cmd
    
    def GetName(self):
        """!Get name"""
        cmd = self.task.get_cmd(ignoreErrors = True)
        if cmd and len(cmd) > 0:
            return cmd[0]
        
        return _('unknown')

    def GetParams(self, dcopy = False):
        """!Get dictionary of parameters"""
        if dcopy:
            return copy.deepcopy(self.task.get_options())
        
        return self.task.get_options()

    def GetTask(self):
        """!Get grassTask instance"""
        return self.task
    
    def SetParams(self, params):
        """!Set dictionary of parameters"""
        self.task.params = params['params']
        self.task.flags  = params['flags']
        
    def MergeParams(self, params):
        """!Merge dictionary of parameters"""
        if 'flags' in params:
            for f in params['flags']:
                self.task.set_flag(f['name'],
                                   f.get('value', False))
        if 'params' in params:
            for p in params['params']:
                self.task.set_param(p['name'],
                                    p.get('value', ''))
        
    def SetValid(self, options):
        """!Set validity for action
        
        @param options dictionary with flags and params (gtask)
        """
        self.isValid = True
        self.isParameterized = False
        
        for f in options['flags']:
            if f.get('parameterized', False):
                self.IsParameterized = True
                break
        
        for p in options['params']:
            if self.isValid and p.get('required', False) and \
                    p.get('value', '') == '' and \
                    p.get('default', '') == '':
                self.isValid = False
            if not self.isParameterized and p.get('parameterized', False):
                self.isParameterized = True
        
        if self.parent.GetCanvas():
            self._setBrush()
            self._setPen()
        
    def IsValid(self):
        """!Check validity (all required parameters set)"""
        return self.isValid
    
    def IsParameterized(self):
        """!Check if action is parameterized"""
        return self.isParameterized
    
    def FindData(self, name):
        """!Find data item by name"""
        for rel in self.GetRelations():
            data = rel.GetData()
            if name == rel.GetName() and name in data.GetName():
                return data
        
        return None

    def Update(self, running = False):
        """!Update action"""
        if running:
            self._setBrush(running = True)
        else:
            self._setBrush()
        self._setPen()

    def OnDraw(self, dc):
        """!Draw action in canvas"""
        self._setBrush()
        self._setPen()
        ogl.RectangleShape.Recentre(self, dc) # re-center text
        ogl.RectangleShape.OnDraw(self, dc)

class ModelData(ModelObject, ogl.EllipseShape):
    def __init__(self, parent, x, y, value = '', prompt = '', width = None, height = None):
        """Data item class
        
        @param parent window parent
        @param x, y   position of the shape
        @param fname, tname list of parameter names from / to
        @param value  value
        @param prompt type of GIS element
        @param width,height dimension of the shape
        """
        ModelObject.__init__(self)
        
        self.parent  = parent
        self.value   = value
        self.prompt  = prompt
        self.intermediate = False
        self.propWin = None
        if not width:
            width = UserSettings.Get(group='modeler', key='data', subkey=('size', 'width'))
        if not height:
            height = UserSettings.Get(group='modeler', key='data', subkey=('size', 'height'))
        
        if self.parent.GetCanvas():
            ogl.EllipseShape.__init__(self, width, height)
            
            self.SetCanvas(self.parent)
            self.SetX(x)
            self.SetY(y)
            self.SetPen(wx.BLACK_PEN)
            self._setBrush()
            
            self._setText()
            
    def IsIntermediate(self):
        """!Checks if data item is intermediate"""
        return self.intermediate
    
    def SetIntermediate(self, im):
        """!Set intermediate flag"""
        self.intermediate = im
  
    def OnDraw(self, dc):
        pen = self.GetPen()
        pen.SetWidth(1)
        if self.intermediate:
            pen.SetStyle(wx.SHORT_DASH)
        else:
            pen.SetStyle(wx.SOLID)
        self.SetPen(pen)
        
        ogl.EllipseShape.OnDraw(self, dc)
        
    def GetLog(self, string = True):
        """!Get logging info"""
        name = list()
        for rel in self.GetRelations():
            name.append(rel.GetName())
        if name:
            return '/'.join(name) + '=' + self.value + ' (' + self.prompt + ')'
        else:
            return self.value + ' (' + self.prompt + ')'

    def GetName(self):
        """!Get list of names"""
        name = list()
        for rel in self.GetRelations():
            name.append(rel.GetName())
        
        return name
    
    def GetPrompt(self):
        """!Get prompt"""
        return self.prompt

    def SetPrompt(self, prompt):
        """!Set prompt
        
        @param prompt
        """
        self.prompt = prompt
        
    def GetValue(self):
        """!Get value"""
        return self.value

    def SetValue(self, value):
        """!Set value

        @param value
        """
        self.value = value
        self._setText()
        for direction in ('from', 'to'):
            for rel in self.GetRelations(direction):
                if direction == 'from':
                    action = rel.GetTo()
                else:
                    action = rel.GetFrom()
                
                task = GUI(show = None).ParseCommand(cmd = action.GetLog(string = False))
                task.set_param(rel.GetName(), self.value)
                action.SetParams(params = task.get_options())
        
    def GetPropDialog(self):
        """!Get properties dialog"""
        return self.propWin

    def SetPropDialog(self, win):
        """!Get properties dialog"""
        self.propWin = win

    def _setBrush(self):
        """!Set brush"""
        if self.prompt == 'raster':
            color = UserSettings.Get(group = 'modeler', key = 'data',
                                     subkey = ('color', 'raster'))
        elif self.prompt == 'raster3d':
            color = UserSettings.Get(group = 'modeler', key = 'data',
                                     subkey = ('color', 'raster3d'))
        elif self.prompt == 'vector':
            color = UserSettings.Get(group = 'modeler', key = 'data',
                                     subkey = ('color', 'vector'))
        else:
            color = UserSettings.Get(group = 'modeler', key = 'action',
                                     subkey = ('color', 'invalid'))
        wxColor = wx.Colour(color[0], color[1], color[2])
        self.SetBrush(wx.Brush(wxColor))
        
    def _setPen(self):
        """!Set pen"""
        isParameterized = False
        for rel in self.GetRelations('from'):
            if rel.GetTo().IsParameterized():
                isParameterized = True
                break
        if not isParameterized:
            for rel in self.GetRelations('to'):
                if rel.GetFrom().IsParameterized():
                    isParameterized = True
                    break

        if isParameterized:
            width = int(UserSettings.Get(group = 'modeler', key = 'action',
                                         subkey = ('width', 'parameterized')))
        else:
            width = int(UserSettings.Get(group = 'modeler', key = 'action',
                                         subkey = ('width', 'default')))
        pen = self.GetPen()
        pen.SetWidth(width)
        self.SetPen(pen)
        
    def _setText(self):
        """!Update text"""
        self.ClearText()
        name = []
        for rel in self.GetRelations():
            name.append(rel.GetName())
        self.AddText('/'.join(name))
        if self.value:
            self.AddText(self.value)
        else:
            self.AddText(_('<not defined>'))
        
    def Update(self):
        """!Update action"""
        self._setBrush()
        self._setPen()
        self._setText()

class ModelRelation(ogl.LineShape):
    """!Data - action relation"""
    def __init__(self, parent, fromShape, toShape, param = ''):
        self.fromShape = fromShape
        self.toShape   = toShape
        self.param     = param
        self.parent    = parent
        
        self._points    = None
        
        if self.parent.GetCanvas():        
            ogl.LineShape.__init__(self)
    
    def __del__(self):
        if self in self.fromShape.rels:
            self.fromShape.rels.remove(self)
        if self in self.toShape.rels:
            self.toShape.rels.remove(self)
        
    def GetFrom(self):
        """!Get id of 'from' shape"""
        return self.fromShape
    
    def GetTo(self):
        """!Get id of 'to' shape"""
        return self.toShape
    
    def GetData(self):
        """!Get related ModelData instance

        @return ModelData instance
        @return None if not found
        """
        if isinstance(self.fromShape, ModelData):
            return self.fromShape
        elif isinstance(self.toShape, ModelData):
            return self.toShape
        
        return None
    
    def GetName(self):
        """!Get parameter name"""
        return self.param
    
    def ResetShapes(self):
        """!Reset related objects"""
        self.fromShape.ResetControlPoints()
        self.toShape.ResetControlPoints()
        self.ResetControlPoints()
        
    def SetControlPoints(self, points):
        """!Set control points"""
        self._points = points
        
    def GetControlPoints(self):
        """!Get list of control points"""
        return self._points
    
    def _setPen(self):
        """!Set pen"""
        pen = self.GetPen()
        pen.SetWidth(1)
        pen.SetStyle(wx.SOLID)
        self.SetPen(pen)
        
    def OnDraw(self, dc):
        """!Draw relation"""
        self._setPen()
        ogl.LineShape.OnDraw(self, dc)
    
    def SetName(self, param):
        self.param = param

class ModelItem(ModelObject):
    def __init__(self, parent, x, y, id = -1, width = None, height = None, text = '', items = []):
        """!Abstract class for loops and conditions"""
        ModelObject.__init__(self, id)
        self.parent  = parent
        self.text    = text
        self.items   = items  # list of items in the loop
        
    def GetText(self):
        """!Get loop text"""
        return self.text

    def GetItems(self):
        """!Get items (id)"""
        return self.items

    def SetId(self, id):
        """!Set loop id"""
        self.id = id

    def SetText(self, cond):
        """!Set loop text (condition)"""
        self.text = cond
        self.ClearText()
        self.AddText('(' + str(self.id) + ') ' + self.text)

    def GetLog(self):
        """!Get log info"""
        if self.text:
            return _("Condition: ") + self.text
        else:
            return _("Condition: not defined")

    def AddRelation(self, rel):
        """!Record relation"""
        self.rels.append(rel)
        
    def Clear(self):
        """!Clear object, remove rels"""
        self.rels = list()
   
class ModelLoop(ModelItem, ogl.RectangleShape):
    def __init__(self, parent, x, y, id = -1, width = None, height = None, text = '', items = []):
        """!Defines a loop"""
        ModelItem.__init__(self, parent, x, y, id, width, height, text, items)
        
        if not width:
            width = UserSettings.Get(group='modeler', key='loop', subkey=('size', 'width'))
        if not height:
            height = UserSettings.Get(group='modeler', key='loop', subkey=('size', 'height'))
        
        if self.parent.GetCanvas():
            ogl.RectangleShape.__init__(self, width, height)
            
            self.SetCanvas(self.parent)
            self.SetX(x)
            self.SetY(y)
            self.SetPen(wx.BLACK_PEN)
            self.SetCornerRadius(100)
            if text:
                self.AddText('(' + str(self.id) + ') ' + text)
            else:
                self.AddText('(' + str(self.id) + ')')
        
        self._setBrush()
        
    def _setBrush(self):
        """!Set brush"""
        if not self.isEnabled:
            color = UserSettings.Get(group='modeler', key='disabled',
                                     subkey='color')
        else:
            color = UserSettings.Get(group='modeler', key='loop',
                                     subkey=('color', 'valid'))
        
        wxColor = wx.Colour(color[0], color[1], color[2])
        self.SetBrush(wx.Brush(wxColor))

    def Enable(self, enabled = True):
        """!Enable/disable action"""
        for item in self.items:
            if not isinstance(item, ModelAction):
                continue
            item.Enable(enabled)
        
        ModelObject.Enable(self, enabled)
        
    def Update(self):
        self._setBrush()
        
    def GetName(self):
        """!Get name"""
        return _("loop")
    
    def SetItems(self, items):
        """!Set items (id)"""
        self.items = items

    def OnDraw(self, dc):
        """!Draw loop in canvas"""
        self._setBrush()
        ogl.RectangleShape.Recentre(self, dc) # re-center text
        ogl.RectangleShape.OnDraw(self, dc)

class ModelCondition(ModelItem, ogl.PolygonShape):
    def __init__(self, parent, x, y, id = -1, width = None, height = None, text = '',
                 items = { 'if' : [], 'else' : [] }):
        """!Defines a if-else condition"""
        ModelItem.__init__(self, parent, x, y, id, width, height, text, items)
        
        if not width:
            self.width = UserSettings.Get(group='modeler', key='if-else', subkey=('size', 'width'))
        else:
            self.width = width
        if not height:
            self.height = UserSettings.Get(group='modeler', key='if-else', subkey=('size', 'height'))
        else:
            self.height = height
        
        if self.parent.GetCanvas():
            ogl.PolygonShape.__init__(self)
            
            points = [(0, - self.height / 2),
                      (self.width / 2, 0),
                      (0, self.height / 2),
                      (- self.width / 2, 0)]
            self.Create(points)
            
            self.SetCanvas(self.parent)
            self.SetX(x)
            self.SetY(y)
            self.SetPen(wx.BLACK_PEN)
            if text:
                self.AddText('(' + str(self.id) + ') ' + text)
            else:
                self.AddText('(' + str(self.id) + ')')

    def GetName(self):
        """!Get name"""
        return _("if-else")

    def GetWidth(self):
        """!Get object width"""
        return self.width

    def GetHeight(self):
        """!Get object height"""
        return self.height

    def SetItems(self, items, branch = 'if'):
        """!Set items (id)

        @param items list of items
        @param branch 'if' / 'else'
        """
        if branch in ['if', 'else']:
            self.items[branch] = items

class ProcessModelFile:
    """!Process GRASS model file (gxm)"""
    def __init__(self, tree):
        """!A ElementTree handler for the GXM XML file, as defined in
        grass-gxm.dtd.
        """
        self.tree = tree
        self.root = self.tree.getroot()
        
        # list of actions, data
        self.properties = dict()
        self.variables  = dict() 
        self.actions = list()
        self.data    = list()
        self.loops   = list()
        self.conditions = list()
        
        self._processWindow()
        self._processProperties()
        self._processVariables()
        self._processItems()
        self._processData()
        
    def _filterValue(self, value):
        """!Filter value
        
        @param value
        """
        value = value.replace('&lt;', '<')
        value = value.replace('&gt;', '>')
        
        return value
        
    def _getNodeText(self, node, tag, default = ''):
        """!Get node text"""
        p = node.find(tag)
        if p is not None:
            if p.text:
                return utils.normalize_whitespace(p.text)
            else:
                return ''
        
        return default
    
    def _processWindow(self):
        """!Process window properties"""
        node = self.root.find('window')
        if node is None:
            self.pos = self.size = None
            return
        
        self.pos, self.size = self._getDim(node)
        
    def _processProperties(self):
        """!Process model properties"""
        node = self.root.find('properties')
        if node is None:
            return
        for key in ('name', 'description', 'author'):
            self._processProperty(node, key)
        
        for f in node.findall('flag'):
            name = f.get('name', '')
            if name == 'overwrite':
                self.properties['overwrite'] = True
        
    def _processProperty(self, pnode, name):
        """!Process given property"""
        node = pnode.find(name)
        if node is not None:
            self.properties[name] = node.text
        else:
            self.properties[name] = ''

    def _processVariables(self):
        """!Process model variables"""
        vnode = self.root.find('variables')
        if vnode is None:
            return
        for node in vnode.findall('variable'):
            name = node.get('name', '')
            if not name:
                continue # should not happen
            self.variables[name] = { 'type' : node.get('type', 'string') }
            for key in ('description', 'value'):
                self._processVariable(node, name, key)
        
    def _processVariable(self, pnode, name, key):
        """!Process given variable"""
        node = pnode.find(key)
        if node is not None:
            if node.text:
                self.variables[name][key] = node.text

    def _processItems(self):
        """!Process model items (actions, loops, conditions)"""
        self._processActions()
        self._processLoops()
        self._processConditions()
        
    def _processActions(self):
        """!Process model file"""
        for action in self.root.findall('action'):
            pos, size = self._getDim(action)
            disabled  = False
            
            task = action.find('task')
            if task is not None:
                if task.find('disabled') is not None:
                    disabled = True
                task = self._processTask(task)
            else:
                task = None
            
            aId = int(action.get('id', -1))
            
            self.actions.append({ 'pos'      : pos,
                                  'size'     : size,
                                  'task'     : task,
                                  'id'       : aId,
                                  'disabled' : disabled })
            
    def _getDim(self, node):
        """!Get position and size of shape"""
        pos = size = None
        posAttr = node.get('pos', None)
        if posAttr:
            posVal = map(int, posAttr.split(','))
            try:
                pos = (posVal[0], posVal[1])
            except:
                pos = None
        
        sizeAttr = node.get('size', None)
        if sizeAttr:
            sizeVal = map(int, sizeAttr.split(','))
            try:
                size = (sizeVal[0], sizeVal[1])
            except:
                size = None
        
        return pos, size        
    
    def _processData(self):
        """!Process model file"""
        for data in self.root.findall('data'):
            pos, size = self._getDim(data)
            param = data.find('data-parameter')
            prompt = value = None
            if param is not None:
                prompt = param.get('prompt', None)
                value = self._filterValue(self._getNodeText(param, 'value'))
            
            if data.find('intermediate') is None:
                intermediate = False
            else:
                intermediate = True
            
            rels = list()
            for rel in data.findall('relation'):
                defrel = { 'id'  : int(rel.get('id', -1)),
                           'dir' : rel.get('dir', 'to'),
                           'name' : rel.get('name', '') }
                points = list()
                for point in rel.findall('point'):
                    x = self._filterValue(self._getNodeText(point, 'x'))
                    y = self._filterValue(self._getNodeText(point, 'y'))
                    points.append((float(x), float(y)))
                defrel['points'] = points
                rels.append(defrel)
            
            self.data.append({ 'pos' : pos,
                               'size': size,
                               'prompt' : prompt,
                               'value' : value,
                               'intermediate' : intermediate,
                               'rels' : rels })
        
    def _processTask(self, node):
        """!Process task

        @return grassTask instance
        @return None on error
        """
        cmd = list()
        parameterized = list()
        
        name = node.get('name', None)
        if not name:
            return None
        
        cmd.append(name)
        
        # flags
        for f in node.findall('flag'):
            flag = f.get('name', '')
            if f.get('parameterized', '0') == '1':
                parameterized.append(('flag', flag))
                if f.get('value', '1') == '0':
                    continue
            if len(flag) > 1:
                cmd.append('--' + flag)
            else:
                cmd.append('-' + flag)
        # parameters
        for p in node.findall('parameter'):
            name = p.get('name', '')
            if p.find('parameterized') is not None:
                parameterized.append(('param', name))
            cmd.append('%s=%s' % (name,
                                  self._filterValue(self._getNodeText(p, 'value'))))
        
        task, err = GUI(show = None, checkError = True).ParseCommand(cmd = cmd)
        if err:
            GWarning(os.linesep.join(err))
        
        for opt, name in parameterized:
            if opt == 'flag':
                task.set_flag(name, True, element = 'parameterized')
            else:
                task.set_param(name, True, element = 'parameterized')
        
        return task

    def _processLoops(self):
        """!Process model loops"""
        for node in self.root.findall('loop'):
            pos, size = self._getDim(node)
            text = self._filterValue(self._getNodeText(node, 'condition')).strip()
            aid = list()
            for anode in node.findall('item'):
                try:
                    aid.append(int(anode.text))
                except ValueError:
                    pass
            
            self.loops.append({ 'pos'     : pos,
                                'size'    : size,
                                'text'    : text,
                                'id'      : int(node.get('id', -1)),
                                'items'   : aid })
        
    def _processConditions(self):
        """!Process model conditions"""
        for node in self.root.findall('if-else'):
            pos, size = self._getDim(node)
            text = self._filterValue(self._getNodeText(node, 'condition')).strip()
            aid = { 'if'   : list(),
                    'else' : list() }
            for b in aid.keys():
                bnode = node.find(b)
                if bnode is None:
                    continue
                for anode in bnode.findall('item'):
                    try:
                        aid[b].append(int(anode.text))
                    except ValueError:
                        pass
            
            self.conditions.append({ 'pos'     : pos,
                                     'size'    : size,
                                     'text'    : text,
                                     'id'      : int(node.get('id', -1)),
                                     'items'   : aid })
        
class WriteModelFile:
    """!Generic class for writing model file"""
    def __init__(self, fd, model):
        self.fd         = fd
        self.model      = model
        self.properties = model.GetProperties()
        self.variables  = model.GetVariables()
        self.items      = model.GetItems()
        
        self.indent = 0
        
        self._header()
        
        self._window()
        self._properties()
        self._variables()
        self._items()
        
        dataList = list()
        for action in model.GetItems(objType = ModelAction):
            for rel in action.GetRelations():
                dataItem = rel.GetData()
                if dataItem not in dataList:
                    dataList.append(dataItem)
        self._data(dataList)
        
        self._footer()

    def _filterValue(self, value):
        """!Make value XML-valid"""
        value = value.replace('<', '&lt;')
        value = value.replace('>', '&gt;')
        
        return value
        
    def _header(self):
        """!Write header"""
        self.fd.write('<?xml version="1.0" encoding="%s"?>\n' % GetDefaultEncoding(forceUTF8 = True))
        self.fd.write('<!DOCTYPE gxm SYSTEM "grass-gxm.dtd">\n')
        self.fd.write('%s<gxm>\n' % (' ' * self.indent))
        self.indent += 4
                
    def _footer(self):
        """!Write footer"""
        self.indent -= 4
        self.fd.write('%s</gxm>\n' % (' ' * self.indent))

    def _window(self):
        """!Write window properties"""
        canvas = self.model.GetCanvas()
        if canvas is None:
            return
        win  = canvas.parent
        pos  = win.GetPosition()
        size = win.GetSize()
        self.fd.write('%s<window pos="%d,%d" size="%d,%d" />\n' % \
                          (' ' * self.indent, pos[0], pos[1], size[0], size[1]))
        
    def _properties(self):
        """!Write model properties"""
        self.fd.write('%s<properties>\n' % (' ' * self.indent))
        self.indent += 4
        if self.properties['name']:
            self.fd.write('%s<name>%s</name>\n' % (' ' * self.indent, EncodeString(self.properties['name'])))
        if self.properties['description']:
            self.fd.write('%s<description>%s</description>\n' % (' ' * self.indent,
                                                                 EncodeString(self.properties['description'])))
        if self.properties['author']:
            self.fd.write('%s<author>%s</author>\n' % (' ' * self.indent,
                                                       EncodeString(self.properties['author'])))
        
        if 'overwrite' in self.properties and \
                self.properties['overwrite']:
            self.fd.write('%s<flag name="overwrite" />\n' % (' ' * self.indent))
        self.indent -= 4
        self.fd.write('%s</properties>\n' % (' ' * self.indent))

    def _variables(self):
        """!Write model variables"""
        if not self.variables:
            return
        self.fd.write('%s<variables>\n' % (' ' * self.indent))
        self.indent += 4
        for name, values in self.variables.iteritems():
            self.fd.write('%s<variable name="%s" type="%s">\n' % \
                              (' ' * self.indent, EncodeString(name), values['type']))
            self.indent += 4
            if 'value' in values:
                self.fd.write('%s<value>%s</value>\n' % \
                                  (' ' * self.indent, EncodeString(values['value'])))
            if 'description' in values:
                self.fd.write('%s<description>%s</description>\n' % \
                                  (' ' * self.indent, EncodeString(values['description'])))
            self.indent -= 4
            self.fd.write('%s</variable>\n' % (' ' * self.indent))
        self.indent -= 4
        self.fd.write('%s</variables>\n' % (' ' * self.indent))
        
    def _items(self):
        """!Write actions/loops/conditions"""
        for item in self.items:
            if isinstance(item, ModelAction):
                self._action(item)
            elif isinstance(item, ModelLoop):
                self._loop(item)
            elif isinstance(item, ModelCondition):
                self._condition(item)
        
    def _action(self, action):
        """!Write actions"""
        self.fd.write('%s<action id="%d" name="%s" pos="%d,%d" size="%d,%d">\n' % \
                          (' ' * self.indent, action.GetId(), action.GetName(), action.GetX(), action.GetY(),
                           action.GetWidth(), action.GetHeight()))
        self.indent += 4
        self.fd.write('%s<task name="%s">\n' % (' ' * self.indent, action.GetLog(string = False)[0]))
        self.indent += 4
        if not action.IsEnabled():
            self.fd.write('%s<disabled />\n' % (' ' * self.indent))
        for key, val in action.GetParams().iteritems():
            if key == 'flags':
                for f in val:
                    if f.get('value', False) or f.get('parameterized', False):
                        if f.get('parameterized', False):
                            if f.get('value', False) == False:
                                self.fd.write('%s<flag name="%s" value="0" parameterized="1" />\n' %
                                              (' ' * self.indent, f.get('name', '')))
                            else:
                                self.fd.write('%s<flag name="%s" parameterized="1" />\n' %
                                              (' ' * self.indent, f.get('name', '')))
                        else:
                            self.fd.write('%s<flag name="%s" />\n' %
                                          (' ' * self.indent, f.get('name', '')))
            else: # parameter
                for p in val:
                    if not p.get('value', '') and not p.get('parameterized', False):
                        continue
                    self.fd.write('%s<parameter name="%s">\n' %
                                  (' ' * self.indent, p.get('name', '')))
                    self.indent += 4
                    if p.get('parameterized', False):
                        self.fd.write('%s<parameterized />\n' % (' ' * self.indent))
                    self.fd.write('%s<value>%s</value>\n' %
                                  (' ' * self.indent, self._filterValue(p.get('value', ''))))
                    self.indent -= 4
                    self.fd.write('%s</parameter>\n' % (' ' * self.indent))
        self.indent -= 4
        self.fd.write('%s</task>\n' % (' ' * self.indent))
        self.indent -= 4
        self.fd.write('%s</action>\n' % (' ' * self.indent))
                
    def _data(self, dataList):
        """!Write data"""
        for data in dataList:
            self.fd.write('%s<data pos="%d,%d" size="%d,%d">\n' % \
                              (' ' * self.indent, data.GetX(), data.GetY(),
                               data.GetWidth(), data.GetHeight()))
            self.indent += 4
            self.fd.write('%s<data-parameter prompt="%s">\n' % \
                              (' ' * self.indent, data.GetPrompt()))
            self.indent += 4
            self.fd.write('%s<value>%s</value>\n' %
                          (' ' * self.indent, self._filterValue(data.GetValue())))
            self.indent -= 4
            self.fd.write('%s</data-parameter>\n' % (' ' * self.indent))
            
            if data.IsIntermediate():
                self.fd.write('%s<intermediate />\n' % (' ' * self.indent))

            # relations
            for ft in ('from', 'to'):
                for rel in data.GetRelations(ft):
                    if ft == 'from':
                        aid = rel.GetTo().GetId()
                    else:
                        aid  = rel.GetFrom().GetId()
                    self.fd.write('%s<relation dir="%s" id="%d" name="%s">\n' % \
                                      (' ' * self.indent, ft, aid, rel.GetName()))
                    self.indent += 4
                    for point in rel.GetLineControlPoints()[1:-1]:
                        self.fd.write('%s<point>\n' % (' ' * self.indent))
                        self.indent += 4
                        x, y = point.Get()
                        self.fd.write('%s<x>%d</x>\n' % (' ' * self.indent, int(x)))
                        self.fd.write('%s<y>%d</y>\n' % (' ' * self.indent, int(y)))
                        self.indent -= 4
                        self.fd.write('%s</point>\n' % (' ' * self.indent))
                    self.indent -= 4
                    self.fd.write('%s</relation>\n' % (' ' * self.indent))
                
            self.indent -= 4
            self.fd.write('%s</data>\n' % (' ' * self.indent))

    def _loop(self, loop):
        """!Write loops"""
        self.fd.write('%s<loop id="%d" pos="%d,%d" size="%d,%d">\n' % \
                          (' ' * self.indent, loop.GetId(), loop.GetX(), loop.GetY(),
                           loop.GetWidth(), loop.GetHeight()))
        text = loop.GetText()
        self.indent += 4
        if text:
            self.fd.write('%s<condition>%s</condition>\n' %
                          (' ' * self.indent, self._filterValue(text)))
        for item in loop.GetItems():
            self.fd.write('%s<item>%d</item>\n' %
                          (' ' * self.indent, item.GetId()))
        self.indent -= 4
        self.fd.write('%s</loop>\n' % (' ' * self.indent))

    def _condition(self, condition):
        """!Write conditions"""
        bbox = condition.GetBoundingBoxMin()
        self.fd.write('%s<if-else id="%d" pos="%d,%d" size="%d,%d">\n' % \
                          (' ' * self.indent, condition.GetId(), condition.GetX(), condition.GetY(),
                           bbox[0], bbox[1]))
        text = condition.GetText()
        self.indent += 4
        if text:
            self.fd.write('%s<condition>%s</condition>\n' %
                          (' ' * self.indent, self._filterValue(text)))
        items = condition.GetItems()
        for b in items.keys():
            if len(items[b]) < 1:
                continue
            self.fd.write('%s<%s>\n' % (' ' * self.indent, b))
            self.indent += 4
            for item in items[b]:
                self.fd.write('%s<item>%d</item>\n' %
                              (' ' * self.indent, item.GetId()))
            self.indent -= 4
            self.fd.write('%s</%s>\n' % (' ' * self.indent, b))
        
        self.indent -= 4
        self.fd.write('%s</if-else>\n' % (' ' * self.indent))
        
class WritePythonFile:
    def __init__(self, fd, model):
        """!Class for exporting model to Python script

        @param fd file desciptor
        """
        self.fd     = fd
        self.model  = model
        self.indent = 4

        self._writePython()
        
    def _writePython(self):
        """!Write model to file"""
        properties = self.model.GetProperties()
        
        self.fd.write(
r"""#!/usr/bin/env python
#
#%s
#
# MODULE:       %s
#
# AUTHOR(S):    %s
#               
# PURPOSE:      %s
#
# DATE:         %s
#
#%s
""" % ('#' * 77,
       EncodeString(properties['name']),
       EncodeString(properties['author']),
       EncodeString('\n# '.join(properties['description'].splitlines())),
       time.asctime(),
       '#' * 77))
        
        self.fd.write(
r"""
import sys
import os
import atexit

import grass.script as grass
""")
        
        # cleanup()
        rast, vect, rast3d, msg = self.model.GetIntermediateData()
        self.fd.write(
r"""
def cleanup():
""")
        if rast:
            self.fd.write(
r"""    grass.run_command('g.remove',
                      rast=%s)
""" % ','.join(map(lambda x: "'" + x + "'", rast)))
        if vect:
            self.fd.write(
r"""    grass.run_command('g.remove',
                      vect = %s)
""" % ','.join(map(lambda x: "'" + x + "'", vect)))
        if rast3d:
            self.fd.write(
r"""    grass.run_command('g.remove',
                      rast3d = %s)
""" % ','.join(map(lambda x: "'" + x + "'", rast3d)))
        if not rast and not vect and not rast3d:
            self.fd.write('    pass\n')
        
        self.fd.write("\ndef main():\n")
        for item in self.model.GetItems():
            self._writePythonItem(item)
        
        self.fd.write("\n    return 0\n")
        
        self.fd.write(
r"""
if __name__ == "__main__":
    options, flags = grass.parser()
    atexit.register(cleanup)
    sys.exit(main())
""")
        
    def _writePythonItem(self, item, ignoreBlock = True, variables = []):
        """!Write model object to Python file"""
        if isinstance(item, ModelAction):
            if ignoreBlock and item.GetBlockId(): # ignore items in loops of conditions
                return
            self._writePythonAction(item, variables = variables)
        elif isinstance(item, ModelLoop) or isinstance(item, ModelCondition):
            # substitute condition
            variables = self.model.GetVariables()
            cond = item.GetText()
            for variable in variables:
                pattern = re.compile('%' + variable)
                if pattern.search(cond):
                    value = variables[variable].get('value', '')
                    if variables[variable].get('type', 'string') == 'string':
                        value = '"' + value + '"'
                    cond = pattern.sub(value, cond)
            if isinstance(item, ModelLoop):
                condVar, condText = map(lambda x: x.strip(), re.split('\s*in\s*', cond))
                cond = "%sfor %s in " % (' ' * self.indent, condVar)
                if condText[0] == '`' and condText[-1] == '`':
                    task = GUI(show = None).ParseCommand(cmd = utils.split(condText[1:-1]))
                    cond += "grass.read_command("
                    cond += self._getPythonActionCmd(task, len(cond), variables = [condVar]) + ".splitlines()"
                else:
                    cond += condText
                self.fd.write('%s:\n' % cond)
                self.indent += 4
                for action in item.GetItems():
                    self._writePythonItem(action, ignoreBlock = False, variables = [condVar])
                self.indent -= 4
            else: # ModelCondition
                self.fd.write('%sif %s:\n' % (' ' * self.indent, cond))
                self.indent += 4
                condItems = item.GetItems()
                for action in condItems['if']:
                    self._writePythonItem(action, ignoreBlock = False)
                if condItems['else']:
                    self.indent -= 4
                    self.fd.write('%selse:\n' % (' ' * self.indent))
                    self.indent += 4
                    for action in condItems['else']:
                        self._writePythonItem(action, ignoreBlock = False)
                self.indent += 4
        
    def _writePythonAction(self, item, variables = []):
        """!Write model action to Python file"""
        task = GUI(show = None).ParseCommand(cmd = item.GetLog(string = False, substitute = self.model.GetVariables()))
        strcmd = "%sgrass.run_command(" % (' ' * self.indent)
        self.fd.write(strcmd + self._getPythonActionCmd(task, len(strcmd), variables) + '\n')
        
    def _getPythonActionCmd(self, task, cmdIndent, variables = []):
        opts = task.get_options()
        
        ret = ''
        flags = ''
        params = list()
        
        for f in opts['flags']:
            if f.get('value', False):
                name = f.get('name', '')
                if len(name) > 1:
                    params.append('%s = True' % name)
                else:
                    flags += name
        
        for p in opts['params']:
            name = p.get('name', None)
            value = p.get('value', None)
            if name and value:
                ptype = p.get('type', 'string')
                if value[0] == '%':
                    params.append("%s = %s" % (name, value[1:]))
                elif ptype == 'string':
                    params.append('%s = "%s"' % (name, value))
                else:
                    params.append("%s = %s" % (name, value))
        
        ret += '"%s"' % task.get_name()
        if flags:
            ret += ",\n%sflags = '%s'" % (' ' * cmdIndent, flags)
        if len(params) > 0:
            ret += ",\n"
            for opt in params[:-1]:
                ret += "%s%s,\n" % (' ' * cmdIndent, opt)
            ret += "%s%s)" % (' ' * cmdIndent, params[-1])
        else:
            ret += ")"
        
        return ret

class ModelParamDialog(wx.Dialog):
    def __init__(self, parent, params, id = wx.ID_ANY, title = _("Model parameters"),
                 style = wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, **kwargs):
        """!Model parameters dialog
        """
        self.parent = parent
        self.params = params
        self.tasks  = list() # list of tasks/pages
        
        wx.Dialog.__init__(self, parent = parent, id = id, title = title, style = style, **kwargs)
        
        self.notebook = GNotebook(parent = self, 
                                  style = globalvar.FNPageDStyle)
        
        panel = self._createPages()
        wx.CallAfter(self.notebook.SetSelection, 0)

        # intermediate data?
        self.interData = wx.CheckBox(parent = self, label = _("Delete intermediate data when finish"))
        self.interData.SetValue(True)
        rast, vect, rast3d, msg = self.parent.GetModel().GetIntermediateData()
        if not rast and not vect and not rast3d:
            self.interData.Hide()
        
        self.btnCancel = wx.Button(parent = self, id = wx.ID_CANCEL)
        self.btnRun    = wx.Button(parent = self, id = wx.ID_OK,
                                   label = _("&Run"))
        self.btnRun.SetDefault()
        
        self._layout()
        
        size = self.GetBestSize()
        self.SetMinSize(size)
        self.SetSize((size.width, size.height +
                      panel.constrained_size[1] -
                      panel.panelMinHeight))
                
    def _layout(self):
        btnSizer = wx.StdDialogButtonSizer()
        btnSizer.AddButton(self.btnCancel)
        btnSizer.AddButton(self.btnRun)
        btnSizer.Realize()
        
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        mainSizer.Add(item = self.notebook, proportion = 1,
                      flag = wx.EXPAND)
        if self.interData.IsShown():
            mainSizer.Add(item = self.interData, proportion = 0,
                          flag = wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, border = 5)

            mainSizer.Add(item = wx.StaticLine(parent = self, id = wx.ID_ANY,
                                               style = wx.LI_HORIZONTAL),
                          proportion = 0,
                          flag = wx.EXPAND | wx.LEFT | wx.RIGHT, border = 5) 
        
        mainSizer.Add(item = btnSizer, proportion = 0,
                      flag = wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, border = 5)
        
        self.SetSizer(mainSizer)
        mainSizer.Fit(self)
        
    def _createPages(self):
        """!Create for each parameterized module its own page"""
        nameOrdered = [''] * len(self.params.keys())
        for name, params in self.params.iteritems():
            nameOrdered[params['idx']] = name
        for name in nameOrdered:
            params = self.params[name]
            panel = self._createPage(name, params)
            if name == 'variables':
                name = _('Variables')
            self.notebook.AddPage(page = panel, text = name)
        
        return panel
    
    def _createPage(self, name, params):
        """!Define notebook page"""
        if name in globalvar.grassCmd:
            task = gtask.grassTask(name)
        else:
            task = gtask.grassTask()
        task.flags  = params['flags']
        task.params = params['params']
        
        panel = CmdPanel(parent = self, id = wx.ID_ANY, task = task)
        self.tasks.append(task)
        
        return panel

    def GetErrors(self):
        """!Check for errors, get list of messages"""
        errList = list()
        for task in self.tasks:
            errList += task.get_cmd_error()
        
        return errList

    def DeleteIntermediateData(self):
        """!Check if to detele intermediate data"""
        if self.interData.IsShown() and self.interData.IsChecked():
            return True
        
        return False
