# -*- coding: utf-8 -*-
# slope.py
"""This module provides a model for Cyclograph"""

# Copyright (C) 2008-2016 Federico Brega, Pierluigi Villani

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

from __future__ import unicode_literals
import math
import glal
from themes import ThemeManager


class Slope:
    """Model of a slope"""
    def __init__(self):
        """Create a slope model."""
        self.cps = []
        self.coords = []
        self.grad = []
        self.dercp = []
        self.name = ''
        self.country = ''
        self.author = ''
        self.email = ''
        self.comment = ''
        self.url = ''
        self.reset_calculated()

    def __len__(self):
        return len(self.cps)

    def reset_calculated(self):
        self.average_grad = 0
        self.max_grad = 0
        self.height_difference = 0
        self.height_gain = 0

    def add_cp(self, distance, altitude, name):
        """ Adds a check-point to a Slope.
        A check point must have an altitude and a distance from start,
       it can have also a name or description.
        """
        rdistance = float('%.3f'%(distance))
        new_cp = (rdistance, altitude, name)
        # check if there is already a cp with the same distance
        # and in this case remove it
        # WARNING: when loading from a file this cause complexity to be O(|#cps|^2)
        # If slowliness occurs consider using a binary search instead.
        for i in range(len(self.cps)):
            if self.cps[i][0] == rdistance:
                del self.cps[i]
                break
        self.cps.append(new_cp)
        self.cps.sort()
        self.grad = []
        self.reset_calculated()
        return self.cps.index(new_cp)

    def add_coord(self, latitude, longitude):
        """ Adds a coordinate to a Slope.
        A coordinate must have latitude and longitude.
        """
        new_coord = (latitude, longitude)
        self.coords.append(new_coord)

    def remove_cp(self, num):
        """ Removes check-point num from current slope and clears data."""
        del self.cps[num]
        self.grad = []
        self.reset_calculated()

    def shift(self, shift_amount):
        for i in range(len(self.cps)):
            self.cps[i][0] += shift_amount

    def calc(self):
        """ Update gradient and altitude bounds"""
        if len(self.cps) > 1:
            cps = self.cps[:]  # Multithread safe?
            derivate = lambda p0, p1: (p1[1] - p0[1])/(p1[0] - p0[0])
            self.grad = [derivate(cps[i], cps[i+1])/10 for i in range(len(cps)-1)]
            self.average_grad = 0
            self.max_grad = self.grad[0]
            for i in range(len(self.grad)):
                if self.grad[i] > self.max_grad:
                    self.max_grad = self.grad[i]
            self.height_gain = 0
            for i in range(len(cps)-1):
                self.height_gain += max(cps[i+1][1]-cps[i][1], 0)
            # find max & min altitude
            # float("inf") doesn't work on Windows
            self.max_h = -float("1e1000")
            self.min_h = +float("1e1000")
            for cpi in self.cps:
                if cpi[1] > self.max_h:
                    self.max_h = cpi[1]
                if cpi[1] < self.min_h:
                    self.min_h = cpi[1]
            self.height_difference = self.max_h - self.min_h
            self.max_h += 100
            self.min_h = int(math.floor(self.min_h/100)) * 100
            # min distance is always in the first item
            self.min_d = self.cps[0][0]
            # max distance is always in the last item
            self.max_d = self.cps[-1][0]

            if self.max_d != 0:
                self.average_grad = (self.cps[-1][1] - self.cps[0][1]) / (self.max_d * 10)

            self.dercp = self.smooth()

    def smooth(self):
        # References:
        #     Subroutine PCHIM, F. N. Fritsch, Lawrence Livermore National Laboratory.
        #     F. N. Fritsch and J. Butland, "A method for constructing local monotone
        #     piecewise cubic interpolants", SIAM J. Sci. Stat. Comput., vol. 5,
        #     pp. 300-304, June 1984.

        if not self.grad:
            return []
        if len(self.cps) < 3:
            # If less than 3 points draw a rect
            return [self.grad[0] * 10] * 2
        der = [0] * len(self.cps)

        # Inspired by Octave code in dpchim.f
        grad1 = self.grad[0] * 10
        grad2 = self.grad[1] * 10
        (h1, h2) = (self.cps[1][0] - self.cps[0][0], self.cps[2][0] - self.cps[1][0])
        w1 = (2 * h1 + h2) / (h1 + h2)
        w2 = -h1 / (h1 + h2)
        der[0] = w1 * grad1 + w2 * grad2
        if der[0] * grad1 <= 0:
            der[0] = 0
        elif grad1 * grad2 < 0:
            dmax = 3 * grad1
            if abs(der[0]) > abs(dmax):
                der[0] = dmax

        # Using brodlie modification of butland's formula
        for i in range(len(self.cps)-2):
            (h1, h2) = (float(self.cps[i][0] - self.cps[i-1][0]), float(self.cps[i+1][0] - self.cps[i][0]))
            grad1 = self.grad[i] * 10
            grad2 = self.grad[i+1] * 10
            if grad1 * grad2 <= 0:
                der[i] = 0
                continue
            dmax = max(abs(grad1), abs(grad2))
            dmin = min(abs(grad1), abs(grad2))
            w1 = (2 * h1 + h2) / (3 * (h1 + h2))
            w2 = (2 * h2 + h1) / (3 * (h1 + h2))
            der[i+1] = dmin / (w1 * grad1 / dmax + w2 * grad2 / dmax)

        grad1 = self.grad[-2] * 10
        grad2 = self.grad[-1] * 10
        (h1, h2) = (self.cps[1][0] - self.cps[0][0], self.cps[2][0] - self.cps[1][0])
        w1 = - h2 / (h1 + h2)
        w2 = (2 * h2 + h1) / (h1 + h2)
        der[-1] = w1 * grad1 + w2 * grad2
        if der[-1] * grad2 <= 0:
            der[-1] = 0
        elif grad1 * grad2 < 0:
            dmax = 3.0 * grad2
            if abs(der[-1]) > abs(dmax):
                der[-1] = dmax
        return der

    depth = 100

    def paint(self, settings, devc):
        """ Paint devc from plot"""
        # upper, lower, right and left margin of area where draw the slope
        theme = ThemeManager().gettheme(settings['theme'])
        updownmar = (180, 30)
        leftrightmar = (50, 10)
        margins = (updownmar, leftrightmar)
        (upp_mar, low_mar) = updownmar
        (lef_mar, rig_mar) = leftrightmar

        theme.paintbackground(devc, devc.size_x, devc.size_y)

        (max_x, max_y) = devc.getsize()
        if settings['3d']:
            devc.shear(theme.shear*self.depth/100)
            min_y = max_x*self.depth/100/10
            rig_mar = 10 + 20*self.depth/100
        else:
            min_y = 0

        theme.gradback(devc, max_x, max_y, settings['fdesc'])

        # draw altitude bar
        metersize = (max_y - upp_mar - low_mar - min_y) \
                    / (self.max_h - self.min_h)
        self.h_incr = 100  # draw a line every 100m

        if settings['olines']:
            theme.olines(devc, self, margins, max_x, max_y, metersize)

        theme.alttext(devc, self, margins, max_y, metersize)
        theme.yaxis(devc, self, margins, max_y, metersize)

        if settings['3d']:
            (dx, dy) = (20*self.depth/100, 10*self.depth/100)
        else:
            (dx, dy) = (0, 0)
        # draw km bar
        devc.setpen('black', 1)
        increments = [1, 2, 5, 10, 20, 50, 100]  # km bar resolutions
        for d_incr in increments:
            # draw less than 30 bars
            if (self.max_d - self.min_d) <= 30 * d_incr:
                break
        # this must be float otherwise there are problems with long slopes
        kmsize = (max_x - lef_mar - rig_mar) / (self.max_d - int(self.min_d))

        theme.xaxis(devc, self, margins, max_y, d_incr, kmsize, dx, dy)

        # draw slope's name
        s_info = (self.name,
                  _("Average gradient:")+" "+"%.1f" % self.average_grad+" %",
                  _("Max gradient:")+" "+"%.1f" % self.max_grad+" %",
                  _("Height difference:")+" "+str(self.height_difference)+" m",
                  _("Height gain")+": "+str(self.height_gain)+" m")
        theme.drawslopeinfo(devc, s_info, settings, lef_mar+20, min_y+upp_mar-140)

        # draw slope's legend
        if settings['legend']:
            theme.drawlegend(devc, settings, max_x-rig_mar-410, min_y+upp_mar-110, _("LEGEND"))

        # draw first info text
        font = settings['fdesc']
        theme.desctext(devc, "%.0f %s" % (self.cps[0][1], self.cps[0][2]),
                     lef_mar + int(self.cps[0][0] * kmsize) + 3,
                     max_y-low_mar-10-int((self.cps[0][1] - self.min_h) * metersize),
                     font)

        # plot the slope
        # plot orizzontal polygon in reverse order to prevent bad visualization in 3d mode
        if (dx != 0) and (dy != 0):
            linkpoints = []
            spath_back = []
            colorlisth = []
            for i in range(len(self.cps)-1):
                # i = len(self.cps)-1 - k
                v_a = (int((self.cps[i][0] - int(self.min_d)) * kmsize),
                       int((self.cps[i][1] - self.min_h) * metersize))
                v_b = (int((self.cps[i+1][0] - int(self.min_d)) * kmsize),
                       int((self.cps[i+1][1] - self.min_h) * metersize))

                points = [(lef_mar + v_a[0], max_y - low_mar - v_a[1]),
                        (lef_mar + v_b[0], max_y - low_mar - v_b[1]),
                        (lef_mar + v_b[0] + dx, max_y - low_mar - v_b[1] + dy),
                        (lef_mar + v_a[0] + dx, max_y - low_mar - v_a[1] + dy)]
                linkpoints.append(points)
                spath_back.append(polytoBezier(points[0],
                  self.dercp[i] * (-metersize / kmsize),
                  points[1],
                  self.dercp[i+1] * (-metersize / kmsize)))
            # theme.fillhslopecontour(devc, spath_back, dx, dy)

            for k in range(len(self.cps)-1):
                i = len(self.cps)-1 - k-1
                color = (theme.getcolor(settings['colors'], settings['levels'], self.grad[i]))
                colorlisth.append(color)
                theme.fillhpoly(devc, linkpoints[i], color)
            theme.fillhslopecontour(devc, spath_back, dx, dy, colorlisth)
            # draw the first polygon
            v_a = (int((self.cps[0][0]-int(self.min_d)) * kmsize),
                   int((self.cps[0][1]-self.min_h) * metersize))

            points = ((lef_mar + v_a[0], max_y - low_mar),
                    (lef_mar + v_a[0], max_y - low_mar - v_a[1]),
                    (lef_mar + v_a[0] + dx, max_y - low_mar - v_a[1] + dy),
                    (lef_mar + v_a[0] + dx, max_y - low_mar + dy))
            color = (theme.getcolor(settings['colors'], settings['levels'], self.grad[0]))
            theme.fillfirsthpoly(devc, points, color)
        vpolygons = []
        spath_pnts = []
        colorlistv = []
        for i in range(len(self.cps)-1):
            v_a = (int((self.cps[i][0] - int(self.min_d)) * kmsize),
                   int((self.cps[i][1] - self.min_h) * metersize))
            v_b = (int((self.cps[i+1][0] - int(self.min_d)) * kmsize),
                   int((self.cps[i+1][1] - self.min_h) * metersize))

            # points that delimitate the area to color
            points = [(lef_mar + v_a[0], max_y - low_mar),
                    (lef_mar + v_b[0], max_y - low_mar),
                    (lef_mar + v_b[0], max_y - low_mar - v_b[1]),
                    (lef_mar + v_a[0], max_y - low_mar - v_a[1])]
            points = [(p[0] + dx, p[1] + dy) for p in points]
            vpolygons.append(points)
            spath_pnts.append(polytoBezier(points[3],
               self.dercp[i] * (-metersize / kmsize),
               points[2],
               self.dercp[i+1] * (-metersize / kmsize)))
            color = (theme.getcolor(settings['colors'], settings['levels'], self.grad[i]))
            colorlistv.append(color)
        # add also the two lower points (those near km bar)
        spath_pnts = [vpolygons[0][0]] + spath_pnts + [vpolygons[-1][1]]
        theme.fillvslopecontour(devc, spath_pnts,
                                max_y - low_mar + self.min_h*metersize,
                                max_y - low_mar - (1000 - self.min_h)*metersize,
                                vpolygons, colorlistv)
        infotext_x = []
        for i in range(len(self.cps)-1):
            points = vpolygons[i]
            color = (theme.getcolor(settings['colors'], settings['levels'], self.grad[i]))
            theme.fillvpoly(devc, points, color)

            # draw gradient text
            font = settings['fgrad']
            if (points[1][0] - points[0][0] > devc.gettextwidth("%.1f%%" % self.grad[i])):
                theme.gradtext(devc, "%.1f%%" % self.grad[i],
                         points[0][0] + 3, points[0][1] - 20,
                         font)
            infotext_x.append(points[2][0] - dx - 4)
        infotext_x.append(infotext_x[len(self.cps)-2]+50)

        for i in range(len(self.cps)-1):
            # another cycle to prevent text to be hidden by polygons
            points = vpolygons[i]

            # draw info text
            font = settings['fdesc']
            infotext = "%.0f %s" % (self.cps[i + 1][1], self.cps[i + 1][2])
            diffx = infotext_x[i+1] - infotext_x[i]
            diffx = diffx - devc.gettextheight(infotext) - 2
            if diffx < 0:
                diffx = diffx/2
                infotext_x[i+1] -= diffx
                theme.desctext(devc, infotext,
                     infotext_x[i] + diffx, points[2][1] - dy - 10, font)
            else:
                theme.desctext(devc, infotext,
                     infotext_x[i], points[2][1] - dy - 10, font)


class SlopeList:
    """Wrapper for a list of slopes, according to MCV"""
    def __init__(self):
        """ Wraps a list of slopes, according to MCV"""
        self._lst = []
        self.message = glal.Message()

    def __len__(self):
        """"Gives how many slopes are in the list"""
        return len(self._lst)

    def new_slope(self):
        """ Add a new slope to the list. """
        self._lst.append(Slope())
        return (len(self._lst) - 1)

    def del_slope(self, slope_number):
        """ Remove a slope from the list """
        del self._lst[slope_number]
        # It doesen't send a SLOPE CHANGED message because
        # other slopes are not been modified.

    def get_slope_copy(self, slope_number):
        """ Get a copy of a slope in the list. """
        return self._lst[slope_number]

    def set_name(self, slope_number, name):
        """ Set the name of a slope in the list. """
        self._lst[slope_number].name = name
        # This updates the title in the tab.
        self.message.send("UPDATE_TAB", slope_number, 0)

    def set_country(self, slope_number, country):
        """ Set the country of a slope in the list."""
        self._lst[slope_number].country = country

    def set_author(self, slope_number, author):
        """ Set the author of a slope in the list."""
        self._lst[slope_number].author = author

    def set_email(self, slope_number, email):
        """ Set the email of the author of a slope."""
        self._lst[slope_number].email = email

    def set_comment(self, slope_number, comment):
        """ Ser a comment to a slope in the list. """
        self._lst[slope_number].comment = comment

    def set_url(self, slope_number, url):
        """ Add a URL referring to a slope in the list """
        self._lst[slope_number].url = url

    def get_name(self, slope_number):
        """ Get name """
        return self._lst[slope_number].name

    def get_state(self, slope_number):
        """ Get country """
        return self._lst[slope_number].country

    def get_author(self, slope_number):
        """ Get author """
        return self._lst[slope_number].author

    def get_email(self, slope_number):
        """ Get email """
        return self._lst[slope_number].email

    def get_comment(self, slope_number):
        """ Get comment """
        return self._lst[slope_number].comment

    def get_average_grad(self, slope_number):
        """ Get average gradient """
        return self._lst[slope_number].average_grad

    def get_max_grad(self, slope_number):
        """ Get max gradient """
        return self._lst[slope_number].max_grad

    def get_height_difference(self, slope_number):
        """ Get height difference"""
        return self._lst[slope_number].height_difference

    def get_height_gain(self, slopenumber):
        """ Get height gain"""
        return self._lst[slopenumber].height_gain

    def get_url(self, slope_number):
        """ Get URL """
        return self._lst[slope_number].url

    def get_coords(self, slope_number):
        """ Get coords """
        return self._lst[slope_number].coords

    def add_coord(self, slope_number, latitude, longitude):
        """ Add a coordinate to a slope in the list."""
        sel_lst = self._lst[slope_number]
        sel_lst.add_coord(latitude, longitude)

    def add_cp(self, slope_number, distance, altitude, name=""):
        """ Add a check point to a slope in the list."""
        sel_lst = self._lst[slope_number]
        orig_len = len(sel_lst)
        row_num = sel_lst.add_cp(distance, altitude, name)
        if len(sel_lst) == orig_len:
            # if the slope isn't grown then a cp has been modified.
            self.message.send("SLOPE_DEL", slope_number, row_num)
        self.message.send("SLOPE_ADD", slope_number, row_num)

    def remove_cp(self, slope_number, cp_num):
        """ Remove a check point from a slope in the list."""
        self._lst[slope_number].remove_cp(cp_num)
        self.message.send("SLOPE_DEL", slope_number, cp_num)

    def shift_slope(self, slope_number, shift_amount):
        sel_lst = self._lst[slope_number]
        sel_lst.shift(shift_amount)
        for row_num in range(len(sel_lst)):
            self.message.send("SLOPE_DEL", slope_number, row_num)
            self.message.send("SLOPE_ADD", slope_number, row_num)

# Below this line fuctions are part of the view according MCV pattern ###


def polytoBezier(p0, m1, p3, m2):
    """Covert from polynomial function to Beziér curve
    p0 is the start point (as tuple of dimension 2) of the Beziér curve
    p1 is the end point of the Beziér curve
    m1 is the value of the derivate in p0
    m2 is the value of the derivate in p3

    returns the four control points of a cubic Beziér curve (p1, p2, p3, p4)
    """
    (x0, y0) = p0
    (x3, y3) = p3
    h = x3 - x0

    x1 = x0 + h/3
    y1 = m1*(x1 - x0) + y0
    p1 = (x1, y1)

    x2 = x0 + 2*h/3
    y2 = m2*(x2 - x3) + y3
    p2 = (x2, y2)

    return (p0, p1, p2, p3)

# vim:sw=4:softtabstop=4:expandtab
