# -*- coding: utf-8 -*-
"""
@package mapwin.analysis

@brief Map display controllers for analyses (profiling, measuring)

Classes:
 - analysis::AnalysisControllerBase
 - analysis::ProfileController
 - analysis::MeasureDistanceController

(C) 2013 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 Anna Petrasova <kratochanna gmail.com>
"""

import os
import math
import wx

from core.utils import _
import core.units as units
from core.gcmd import RunCommand
from core.giface import Notification

from grass.pydispatch.signal import Signal
from grass.script.utils import parse_key_val


class AnalysisControllerBase:
    """Base class for analysis which require drawing line in map display."""

    def __init__(self, giface, mapWindow):
        """

        :param giface: grass interface
        :param mapWindow: instance of BufferedMapWindow
        """
        self._giface = giface
        self._mapWindow = mapWindow

        self._registeredGraphics = None
        self._graphicsType = None

        self._oldMouseUse = None
        self._oldCursor = None

    def IsActive(self):
        """Returns True if analysis mode is activated."""
        return bool(self._registeredGraphics)

    def _start(self, x, y):
        """Handles the actual start of drawing line
        and adding each new point.

        :param x,y: east north coordinates
        """
        if not self._registeredGraphics.GetAllItems():
            item = self._registeredGraphics.AddItem(coords=[[x, y]])
            item.SetPropertyVal('penName', 'analysisPen')
        else:
            # needed to switch mouse begin and end to draw intermediate line
            # properly
            coords = self._registeredGraphics.GetItem(0).GetCoords()[-1]
            self._mapWindow.mouse['begin'] = self._mapWindow.Cell2Pixel(coords)

    def _addPoint(self, x, y):
        """New point added.

        :param x,y: east north coordinates
        """
        # add new point and calculate distance
        item = self._registeredGraphics.GetItem(0)
        coords = item.GetCoords() + [[x, y]]
        item.SetCoords(coords)
        # draw
        self._mapWindow.ClearLines()
        self._registeredGraphics.Draw()
        self._mapWindow.Refresh()
        wx.SafeYield()

        self._doAnalysis(coords)

    def _doAnalysis(self, coords):
        """Perform the required analysis
        (compute distnace, update profile)

        :param coords: EN coordinates
        """
        raise NotImplementedError()

    def _disconnectAll(self):
        """Disconnect all mouse signals
        to stop drawing."""
        raise NotImplementedError()

    def _connectAll(self):
        """Connect all mouse signals to draw."""
        raise NotImplementedError()

    def _getPen(self):
        """Returns wx.Pen instance."""
        raise NotImplementedError()

    def Stop(self, restore=True):
        """Analysis mode is stopped.

        :param restore: if restore previous cursor, mouse['use']
        """
        self._mapWindow.ClearLines(pdc=self._mapWindow.pdcTmp)
        self._mapWindow.mouse['end'] = self._mapWindow.mouse['begin']
        # disconnect mouse events
        self._disconnectAll()
        # unregister
        self._mapWindow.UnregisterGraphicsToDraw(self._registeredGraphics)
        self._registeredGraphics = None
        self._mapWindow.UpdateMap(render=False)

        if restore:
            # restore mouse['use'] and cursor to the state before measuring
            # starts
            self._mapWindow.SetNamedCursor(self._oldCursor)
            self._mapWindow.mouse['use'] = self._oldMouseUse

    def Start(self):
        """Init analysis: register graphics to map window,
        connect required mouse signals.
        """
        self._oldMouseUse = self._mapWindow.mouse['use']
        self._oldCursor = self._mapWindow.GetNamedCursor()

        self._registeredGraphics = self._mapWindow.RegisterGraphicsToDraw(
            graphicsType=self._graphicsType, mapCoords=True)

        self._connectAll()

        # change mouse['box'] and pen to draw line during dragging
        # TODO: better solution for drawing this line
        self._mapWindow.mouse['use'] = None
        self._mapWindow.mouse['box'] = "line"
        self._mapWindow.pen = wx.Pen(
            colour='red', width=2, style=wx.SHORT_DASH)

        self._registeredGraphics.AddPen('analysisPen', self._getPen())

        # change the cursor
        self._mapWindow.SetNamedCursor('pencil')


class ProfileController(AnalysisControllerBase):
    """Class controls profiling in map display.
    It should be used inside ProfileFrame
    """

    def __init__(self, giface, mapWindow):
        AnalysisControllerBase.__init__(
            self, giface=giface, mapWindow=mapWindow)

        self.transectChanged = Signal('ProfileController.transectChanged')
        self._graphicsType = 'line'

    def _doAnalysis(self, coords):
        """Informs profile dialog that profile changed.

        :param coords: EN coordinates
        """
        self.transectChanged.emit(coords=coords)

    def _disconnectAll(self):
        self._mapWindow.mouseLeftDown.disconnect(self._start)
        self._mapWindow.mouseLeftUp.disconnect(self._addPoint)

    def _connectAll(self):
        self._mapWindow.mouseLeftDown.connect(self._start)
        self._mapWindow.mouseLeftUp.connect(self._addPoint)

    def _getPen(self):
        return wx.Pen(
            colour=wx.Colour(0, 100, 0),
            width=2, style=wx.SHORT_DASH)

    def Stop(self, restore=True):
        AnalysisControllerBase.Stop(self, restore=restore)

        self.transectChanged.emit(coords=[])


class MeasureDistanceController(AnalysisControllerBase):
    """Class controls measuring distance in map display."""

    def __init__(self, giface, mapWindow):
        AnalysisControllerBase.__init__(
            self, giface=giface, mapWindow=mapWindow)

        self._projInfo = self._mapWindow.Map.projinfo
        self._totaldist = 0.0  # total measured distance
        self._useCtypes = False
        self._graphicsType = 'line'

    def _doAnalysis(self, coords):
        """New point added.

        :param x,y: east north coordinates
        """
        self.MeasureDist(coords[-2], coords[-1])

    def _disconnectAll(self):
        self._mapWindow.mouseLeftDown.disconnect(self._start)
        self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
        self._mapWindow.mouseDClick.disconnect(self.Stop)

    def _connectAll(self):
        self._mapWindow.mouseLeftDown.connect(self._start)
        self._mapWindow.mouseLeftUp.connect(self._addPoint)
        self._mapWindow.mouseDClick.connect(self.Stop)

    def _getPen(self):
        return wx.Pen(colour='green', width=2, style=wx.SHORT_DASH)

    def Stop(self, restore=True):
        if not self.IsActive():
            return
        AnalysisControllerBase.Stop(self, restore=restore)

        self._giface.WriteCmdLog(_('Measuring finished'))

    def Start(self):
        """Init measurement routine that calculates map distance
        along transect drawn on map display
        """
        if self.IsActive():
            return
        AnalysisControllerBase.Start(self)
        self._totaldist = 0.0  # total measured distance

        # initiating output (and write a message)
        # e.g., in Layer Manager switch to output console
        # TODO: this should be something like: write important message or write tip
        # TODO: mixed 'switching' and message? no, measuring handles 'swithing'
        # on its own
        self._giface.WriteWarning(
            _(
                'Click and drag with left mouse button '
                'to measure.%s'
                'Double click with left button to clear.') %
            (os.linesep))
        if self._projInfo['proj'] != 'xy':
            mapunits = self._projInfo['units']
            self._giface.WriteCmdLog(_('Measuring distance') + ' ('
                                     + mapunits + '):')
        else:
            self._giface.WriteCmdLog(_('Measuring distance:'))

        if self._projInfo['proj'] == 'll':
            try:
                import grass.lib.gis as gislib
                gislib.G_begin_distance_calculations()
                self._useCtypes = True
            except ImportError as e:
                self._giface.WriteWarning(_('Geodesic distance calculation '
                                            'is not available.\n'
                                            'Reason: %s' % e))

    def MeasureDist(self, beginpt, endpt):
        """Calculate distance and print to output window.

        :param beginpt,endpt: EN coordinates
        """
        # move also Distance method?
        dist, (north, east) = self._mapWindow.Distance(
            beginpt, endpt, screen=False)

        dist = round(dist, 3)
        mapunits = self._projInfo['units']
        if mapunits == 'degrees' and self._useCtypes:
            mapunits = 'meters'
        d, dunits = units.formatDist(dist, mapunits)

        self._totaldist += dist
        td, tdunits = units.formatDist(self._totaldist,
                                       mapunits)
        if dunits == 'units' and mapunits:
            dunits = tdunits = mapunits

        strdist = str(d)
        strtotdist = str(td)

        if self._projInfo[
                'proj'] == 'xy' or 'degree' not in self._projInfo['unit']:
            angle = int(math.degrees(math.atan2(north, east)) + 0.5)
            # uncomment below (or flip order of atan2(y,x) above) to use
            #   the mathematical theta convention (CCW from +x axis)
            #angle = 90 - angle
            if angle < 0:
                angle = 360 + angle

            mstring = '%s = %s %s\n%s = %s %s\n%s = %d %s\n%s' % (
                _('segment'),
                strdist, dunits, _('total distance'),
                strtotdist, tdunits, _('bearing'),
                angle, _('degrees (clockwise from grid-north)'),
                '-' * 60)
        else:
            mstring = '%s = %s %s\n%s = %s %s\n%s' \
                % (_('segment'), strdist, dunits,
                   _('total distance'), strtotdist, tdunits,
                   '-' * 60)

        self._giface.WriteLog(mstring, notification=Notification.MAKE_VISIBLE)

        return dist


class MeasureAreaController(AnalysisControllerBase):
    """Class controls measuring area in map display."""

    def __init__(self, giface, mapWindow):
        AnalysisControllerBase.__init__(
            self, giface=giface, mapWindow=mapWindow)
        self._graphicsType = 'polygon'

    def _doAnalysis(self, coords):
        """New point added.

        :param coords: east north coordinates as a list
        """
        self.MeasureArea(coords)

    def _disconnectAll(self):
        self._mapWindow.mouseLeftDown.disconnect(self._start)
        self._mapWindow.mouseLeftUp.disconnect(self._addPoint)
        self._mapWindow.mouseDClick.disconnect(self.Stop)

    def _connectAll(self):
        self._mapWindow.mouseLeftDown.connect(self._start)
        self._mapWindow.mouseLeftUp.connect(self._addPoint)
        self._mapWindow.mouseDClick.connect(self.Stop)

    def _getPen(self):
        return wx.Pen(colour='green', width=2, style=wx.SOLID)

    def Stop(self, restore=True):
        if not self.IsActive():
            return
        AnalysisControllerBase.Stop(self, restore=restore)

        self._giface.WriteCmdLog(_('Measuring finished'))

    def Start(self):
        """Init measurement routine that calculates area of polygon
        drawn on map display.
        """
        if self.IsActive():
            return
        AnalysisControllerBase.Start(self)

        self._giface.WriteWarning(
            _(
                'Click and drag with left mouse button '
                'to measure.%s'
                'Double click with left button to clear.') %
            (os.linesep))
        self._giface.WriteCmdLog(_('Measuring area:'))

    def MeasureArea(self, coords):
        """Calculate area and print to output window.

        :param coords: list of E, N coordinates
        """
        # TODO: make sure appending first point is needed for m.measure
        coordinates = coords + [coords[0]]
        coordinates = ','.join([str(item)
                                for sublist in coordinates for item in
                                sublist])
        result = RunCommand(
            'm.measure',
            flags='g',
            coordinates=coordinates,
            read=True).strip()
        result = parse_key_val(result)
        if 'units' not in result:
            self._giface.WriteWarning(
                _("Units not recognized, measurement failed."))
            unit = ''
        else:
            unit = result['units'].split(',')[1]
        if 'area' not in result:
            text = _("Area: {area} {unit}\n").format(area=0, unit=unit)
        else:
            text = _("Area: {area} {unit}\n").format(
                area=result['area'], unit=unit)
        self._giface.WriteLog(text, notification=Notification.MAKE_VISIBLE)
