"""
@package mapdisp.main

@brief Start Map Display as standalone application

Classes:
 - mapdisp::DMonMap
 - mapdisp::Layer
 - mapdisp::LayerList
 - mapdisp::DMonGrassInterface
 - mapdisp::DMonFrame
 - mapdisp::MapApp

Usage:
python mapdisp/main.py monitor-identifier /path/to/map/file /path/to/command/file /path/to/env/file

(C) 2006-2014 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 Michael Barton
@author Jachym Cepicky
@author Martin Landa <landa.martin gmail.com>
@author Vaclav Petras <wenzeslaus gmail.com> (MapFrameBase)
@author Anna Kratochvilova <kratochanna gmail.com> (MapFrameBase)
"""

import os
import sys

from core          import globalvar
import wx

from core          import utils
from core.giface   import StandaloneGrassInterface
from core.gcmd     import RunCommand
from core.render   import Map, MapLayer
from core.utils import _
from mapdisp.frame import MapFrame
from core.debug    import Debug
from core.settings import UserSettings

from grass.script.utils import try_remove
from grass.script import core as grass
from grass.pydispatch.signal import Signal

# for standalone app
monFile = { 'cmd' : None,
            'map' : None,
            'env' : None,
            }
monName = None
monSize = list(globalvar.MAP_WINDOW_SIZE)

class DMonMap(Map):
    def __init__(self, giface, cmdfile=None, mapfile=None):
        """Map composition (stack of map layers and overlays)

        :param cmdline: full path to the cmd file (defined by d.mon)
        :param mapfile: full path to the map file (defined by d.mon)
        """

        Map.__init__(self)

        self._giface = giface

        # environment settings
        self.env   = dict()

        self.cmdfile = cmdfile

        # list of layers for rendering added from cmd file
        # TODO temporary solution, layer managment by different tools in GRASS should be resovled
        self.ownedLayers = []

        if mapfile:
            self.mapfileCmd = mapfile
            self.maskfileCmd = os.path.splitext(mapfile)[0] + '.pgm'

        # generated file for g.pnmcomp output for rendering the map
        self.mapfile = monFile['map']
        if os.path.splitext(self.mapfile)[1] != '.ppm':
            self.mapfile += '.ppm'
        
        # signal sent when d.out.file/d.to.rast appears in cmd file, attribute is cmd
        self.saveToFile = Signal('DMonMap.saveToFile')
        self.dToRast = Signal('DMonMap.dToRast')
        # signal sent when d.what.rast/vect appears in cmd file, attribute is cmd
        self.query = Signal('DMonMap.query')

    def GetLayersFromCmdFile(self):
        """Get list of map layers from cmdfile
        """
        if not self.cmdfile:
            return

        nlayers = 0

        try:
            fd = open(self.cmdfile, 'r')
            lines = fd.readlines()
            fd.close()
            # detect d.out.file, delete the line from the cmd file and export graphics
            if len(lines) > 0:
                if lines[-1].startswith('d.out.file') or lines[-1].startswith('d.to.rast'):
                    dCmd = lines[-1].strip()
                    fd = open(self.cmdfile, 'w')
                    fd.writelines(lines[:-1])
                    fd.close()
                    if lines[-1].startswith('d.out.file'):
                        self.saveToFile.emit(cmd=utils.split(dCmd))
                    else:
                        self.dToRast.emit(cmd=utils.split(dCmd))
                    return
                if lines[-1].startswith('d.what'):
                    dWhatCmd = lines[-1].strip()
                    fd = open(self.cmdfile, 'w')
                    fd.writelines(lines[:-1])
                    fd.close()
                    if '=' in utils.split(dWhatCmd)[1]:
                        maps = utils.split(dWhatCmd)[1].split('=')[1].split(',')
                    else:
                        maps = utils.split(dWhatCmd)[1].split(',')
                    self.query.emit(ltype=utils.split(dWhatCmd)[0].split('.')[-1], maps=maps)
                    return

            existingLayers = self.GetListOfLayers()

            # holds new rendreing order for every layer in existingLayers
            layersOrder = [-1] * len(self.GetListOfLayers())

            # next number in rendering order
            next_layer = 0

            for line in lines:
                cmd = utils.split(line.strip())
                ltype = None

                try:
                    ltype = utils.command2ltype[cmd[0]]
                except KeyError:
                    grass.warning(_("Unsupported command %s.") % cmd[0])
                    continue

                name = utils.GetLayerNameFromCmd(cmd, fullyQualified = True,
                                                 layerType = ltype)[0]

                # creating temporary layer object to compare commands
                # neccessary to get the same format
                # supposing that there are no side effects
                tmpMapLayer = MapLayer(ltype = ltype, name = name,
                                       cmd = cmd, Map = None,
                                       active = False, hidden = True,
                                       opacity = 0)
                exists = False
                for i, layer in enumerate(existingLayers):
                    if layer.GetCmd(string=True) == tmpMapLayer.GetCmd(string=True):
                        exists = True

                        if layersOrder[i] == -1: 
                            layersOrder[i] = next_layer;
                            next_layer += 1
                        # layer must be put higher in render order (same cmd was insered more times)
                        # TODO delete rendurant cmds from cmd file?
                        else:
                            for j, l_order in enumerate(layersOrder):
                                if l_order > layersOrder[i]:
                                    layersOrder[j] -= 1;
                            layersOrder[i] = next_layer - 1;

                        break
                if exists:
                    continue

                newLayer = Map.AddLayer(self, ltype = ltype, command = cmd, active = True, name = name)
                
                existingLayers.append(newLayer)
                self.ownedLayers.append(newLayer)

                layersOrder.append(next_layer)
                next_layer += 1

                nlayers += 1

            reorderedLayers = [-1] * next_layer
            for i, layer in enumerate(existingLayers):

                # owned layer was not found in cmd file -> is deleted 
                if layersOrder[i] == -1 and layer in self.ownedLayers:
                    self.ownedLayers.remove(layer)
                    self.DeleteLayer(layer)

                # other layer e. g. added by wx.vnet are added to the top
                elif layersOrder[i] == -1 and layer not in self.ownedLayers:
                    reorderedLayers.append(layer)
                
                # owned layer found in cmd file is added into proper rendering position
                else:
                    reorderedLayers[layersOrder[i]] = layer

            self.SetLayers(reorderedLayers)

        except IOError as e:
            grass.warning(_("Unable to read cmdfile '%(cmd)s'. Details: %(det)s") % \
                              { 'cmd' : self.cmdfile, 'det' : e })
            return

        self._giface.updateMap.emit()

        Debug.msg(1, "Map.GetLayersFromCmdFile(): cmdfile=%s" % self.cmdfile)
        Debug.msg(1, "                            nlayers=%d" % nlayers)
                
    def Render(self, *args, **kwargs):
        """Render layer to image.

        For input params and returned data see overridden method in Map class.
        """
        ret = Map.Render(self, *args, **kwargs)

        return ret
    
    def AddLayer(self, *args, **kwargs):
        """Adds generic map layer to list of layers.

        For input params and returned data see overridden method in Map class.
        """
        driver = UserSettings.Get(group = 'display', key = 'driver', subkey = 'type')
    
        if driver == 'png':
            os.environ["GRASS_RENDER_IMMEDIATE"] = "png"
        else:
            os.environ["GRASS_RENDER_IMMEDIATE"] = "cairo"

        layer = Map.AddLayer(self, *args, **kwargs)

        del os.environ["GRASS_RENDER_IMMEDIATE"]

        return layer


class Layer(object):
    """@implements core::giface::Layer"""
    def __init__(self, maplayer):
        self._maplayer = maplayer

    def __getattr__(self, name):
        if name == 'cmd':
            return utils.CmdTupleToList(self._maplayer.GetCmd())
        elif hasattr(self._maplayer, name):
            return getattr(self._maplayer, name)
        elif name == 'maplayer':
            return self._maplayer
        elif name == 'type':
            return self._maplayer.GetType()
            #elif name == 'ctrl':
        elif name == 'label':
            return self._maplayer.GetName()
            #elif name == 'propwin':


class LayerList(object):
    def __init__(self, map, giface):
        """@implements core::giface::LayerList"""
        self._map = map
        self._giface = giface

    def GetSelectedLayers(self, checkedOnly=True):
        # hidden and selected vs checked and selected
        items = self._map.GetListOfLayers()
        layers = []
        for item in items:
            layer = Layer(item)
            layers.append(layer)
        return layers

    def GetSelectedLayer(self, checkedOnly=False):
        """Returns selected layer or None when there is no selected layer."""
        layers = self.GetSelectedLayers()
        if len(layers) > 0:
            return layers[0]
        else:
            return None

    def AddLayer(self, ltype, name=None, checked=None,
                 opacity=1.0, cmd=None):
        """Adds a new layer to the layer list.

        Launches property dialog if needed (raster, vector, etc.)

        :param ltype: layer type (raster, vector, 3d-raster, ...)
        :param name: layer name
        :param checked: if True layer is checked
        :param opacity: layer opacity level
        :param cmd: command (given as a list)
        """
        self._map.AddLayer(ltype=ltype, command=cmd,
                           name=name, active=True,
                           opacity=opacity, render=True,
                           pos=-1)
        # TODO: this should be solved by signal
        # (which should be introduced everywhere,
        # alternative is some observer list)
        self._giface.updateMap.emit(render=True, renderVector=True)

    def GetLayersByName(self, name):
        items = self._map.GetListOfLayers()
        layers = []
        for item in items:
            if item.GetName() == name:
                layer = Layer(item)
                layers.append(layer)
        return layers

    def GetLayerByData(self, key, value):
        # TODO: implementation was not tested
        items = self._map.GetListOfLayers()
        for item in items:
            layer = Layer(item)
            try:
                if getattr(layer, key) == value:
                    return layer
            except AttributeError:
                pass
        return None


class DMonGrassInterface(StandaloneGrassInterface):
    """@implements GrassInterface"""
    def __init__(self, mapframe):
        StandaloneGrassInterface.__init__(self)
        self._mapframe = mapframe

    def GetLayerList(self):
        return LayerList(self._mapframe.GetMap(), giface=self)

    def GetMapWindow(self):
        return self._mapframe.GetMapWindow()

    def GetProgress(self):
        return self._mapframe.GetProgressBar()


class DMonFrame(MapFrame):
    def OnZoomToMap(self, event):
        layers = self.MapWindow.GetMap().GetListOfLayers()
        self.MapWindow.ZoomToMap(layers = layers)
        

class MapApp(wx.App):
    def OnInit(self):
        if not globalvar.CheckWxVersion([2, 9]):
            wx.InitAllImageHandlers()

        grass.set_raise_on_error(True)
        # actual use of StandaloneGrassInterface not yet tested
        # needed for adding functionality in future
        giface = DMonGrassInterface(None)

        if __name__ == "__main__":
            self.cmdTimeStamp = os.path.getmtime(monFile['cmd'])
            self.Map = DMonMap(giface=giface, cmdfile=monFile['cmd'],
                               mapfile = monFile['map'])
        else:
            self.Map = None

        self.mapFrm = DMonFrame(parent = None, id = wx.ID_ANY, Map = self.Map,
                                giface = giface, size = monSize)
        # FIXME: hack to solve dependency
        giface._mapframe = self.mapFrm
        # self.SetTopWindow(Map)
        self.mapFrm.GetMapWindow().SetAlwaysRenderEnabled(True)
        self.Map.saveToFile.connect(lambda cmd: self.mapFrm.DOutFile(cmd))
        self.Map.dToRast.connect(lambda cmd: self.mapFrm.DToRast(cmd))
        self.Map.query.connect(lambda ltype, maps: self.mapFrm.SetQueryLayersAndActivate(ltype=ltype, maps=maps))
        self.mapFrm.Show()
        
        if __name__ == "__main__":
            self.timer = wx.PyTimer(self.watcher)
            #check each 0.5s
            global mtime
            mtime = 500
            self.timer.Start(mtime)
            
        return True
    
    def OnExit(self):
        if __name__ == "__main__":
            # stop the timer
            # self.timer.Stop()
            # terminate thread
            for f in monFile.itervalues():
                try_remove(f)
            
    def watcher(self):
        """Redraw, if new layer appears (check's timestamp of
        cmdfile)
        """
        try:
            # GISBASE and other system enviromental variables can not be used
            # since the process inherited them from GRASS
            # raises exception when vaiable does not exists
            grass.gisenv()['GISDBASE']
        except KeyError:
            self.timer.Stop()
            return
        
        # todo: events
        try:
            currentCmdFileTime = os.path.getmtime(monFile['cmd'])
            if currentCmdFileTime > self.cmdTimeStamp:
                self.timer.Stop()
                self.cmdTimeStamp = currentCmdFileTime
                self.mapFrm.GetMap().GetLayersFromCmdFile()
                self.timer.Start(mtime)
        except OSError as e:
            grass.warning("%s" % e)
            self.timer.Stop()

    def GetMapFrame(self):
        """Get Map Frame instance"""
        return self.mapFrm

if __name__ == "__main__":
    # set command variable
    if len(sys.argv) < 5:
        print __doc__
        sys.exit(1)
    
    monName = sys.argv[1]
    monFile = { 'map' : sys.argv[2],
                'cmd' : sys.argv[3],
                'env' : sys.argv[4],
                }
    if len(sys.argv) >= 6:
        try:
            monSize[0] = int(sys.argv[5])
        except ValueError:
            pass
    
    if len(sys.argv) == 7:
        try:
            monSize[1] = int(sys.argv[6])
        except ValueError:
            pass

    
    grass.verbose(_("Starting map display <%s>...") % (monName))

    RunCommand('g.gisenv',
               set = 'MONITOR_%s_PID=%d' % (monName.upper(), os.getpid()))
    
    gmMap = MapApp(0)
    # set title
    gmMap.mapFrm.SetTitle(monName)
    
    gmMap.MainLoop()
    
    grass.verbose(_("Stopping map display <%s>...") % (monName))

    # clean up GRASS env variables
    env = grass.gisenv()
    env_name = 'MONITOR_%s' % monName.upper()
    unset = list()
    for key in env.keys():
        if key.find(env_name) == 0 or \
           (key == 'MONITOR' and env[key] == monName):
            unset.append(key)
    if unset:
        RunCommand('g.gisenv',
                   unset = '%s' % ','.join(unset))
    
    sys.exit(0)
