"""
@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 import 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
        """
        # avoid duplicating point in the beginning
        begin = self._mapWindow.mouse["begin"]
        end = self._mapWindow.mouse["end"]
        if begin == end:
            return
        # 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 distance, 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.\nReason: %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

            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)
