# -*- coding: utf-8 -*-

# Copyright 2017 Juan José Martín Miralles
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# Python modules
import math
import warnings
import datetime
import datetime as dt
from datetime import datetime

# Third party modules
import geopy
import geopy.distance as geo_dist
import pandas as pd

TIME_FORMATS = ['%H:%M', '%H%M', '%I:%M%p', '%I%M%p', '%H:%M:%S', '%H%M%S', '%I:%M:%S%p', '%I%M%S%p']


class TrackException(Exception):
    """
    Generic exception for TrackAnimation
    """

    def __init__(self, msg, original_exception):
        super(TrackException, self).__init__(msg + (": %s" % original_exception))
        self.original_exception = original_exception


def getBearing(start_point, end_point):
    """
    Calculates the bearing between two points.

    Parameters
    ----------
    start_point: geopy.Point
    end_point: geopy.Point

    Returns
    -------
    point: int
        Bearing in degrees between the start and end points.
    """
    warnings.warn("The getBearing function is deprecated and "
                  "will be removed in version 2.0.0. "
                  "Use the get_bearing function instead.",
                  FutureWarning,
                  stacklevel=8
                  )
    return get_bearing(start_point, end_point)


def get_bearing(start_point, end_point):
    """
    Calculates the bearing between two points.

    Parameters
    ----------
    start_point: geopy.Point
    end_point: geopy.Point

    Returns
    -------
    point: int
        Bearing in degrees between the start and end points.
    """
    start_lat = math.radians(start_point.latitude)
    start_lng = math.radians(start_point.longitude)
    end_lat = math.radians(end_point.latitude)
    end_lng = math.radians(end_point.longitude)

    d_lng = end_lng - start_lng
    if abs(d_lng) > math.pi:
        if d_lng > 0.0:
            d_lng = -(2.0 * math.pi - d_lng)
        else:
            d_lng = (2.0 * math.pi + d_lng)

    tan_start = math.tan(start_lat / 2.0 + math.pi / 4.0)
    tan_end = math.tan(end_lat / 2.0 + math.pi / 4.0)
    d_phi = math.log(tan_end / tan_start)
    bearing = (math.degrees(math.atan2(d_lng, d_phi)) + 360.0) % 360.0

    return bearing


def getCoordinates(start_point, end_point, distance_meters):
    """
    Calculates the new coordinates between two points depending
    of the specified distance and the calculated bearing.

    Parameters
    ----------
    start_point: geopy.Point
    end_point: geopy.Point
    distance_meters: float

    Returns
    -------
    point: geopy.Point
        A new point between the start and the end points.
    """
    warnings.warn("The getCoordinates function is deprecated and "
                  "will be removed in version 2.0.0. "
                  "Use the get_coordinates function instead.",
                  FutureWarning,
                  stacklevel=8
                  )
    return get_coordinates(start_point, end_point, distance_meters)


def get_coordinates(start_point, end_point, distance_meters):
    """
    Calculates the new coordinates between two points depending
    of the specified distance and the calculated bearing.

    Parameters
    ----------
    start_point: geopy.Point
    end_point: geopy.Point
    distance_meters: float

    Returns
    -------
    point: geopy.Point
        A new point between the start and the end points.
    """
    bearing = get_bearing(start_point, end_point)

    distance_km = distance_meters / 1000
    d = geo_dist.VincentyDistance(kilometers=distance_km)
    destination = d.destination(point=start_point, bearing=bearing)

    return geopy.Point(destination.latitude, destination.longitude)


def getPointInTheMiddle(start_point, end_point, time_diff, point_idx):
    """
    Calculates a new point between two points depending of the
    time difference between them and the point index.

    Parameters
    ----------
    start_point: DataFrame
    end_point: DataFrame
    time_diff: float
    point_idx: int
        Point index between the start and the end points

    Returns
    -------
    point: list
        A new point between the start and the end points.
    """
    warnings.warn("The getPointInTheMiddle function is deprecated and "
                  "will be removed in version 2.0.0. "
                  "Use the get_point_in_the_middle function instead.",
                  FutureWarning,
                  stacklevel=8
                  )
    return get_point_in_the_middle(start_point, end_point, time_diff, point_idx)


def get_point_in_the_middle(start_point, end_point, time_diff, point_idx):
    """
    Calculates a new point between two points depending of the
    time difference between them and the point index.

    Parameters
    ----------
    start_point: DataFrame
    end_point: DataFrame
    time_diff: float
    point_idx: int
        Point index between the start and the end points

    Returns
    -------
    point: list
        A new point between the start and the end points.
    """
    time_proportion = (time_diff * point_idx) / end_point['TimeDifference'].item()

    distance_proportion = end_point['Distance'].item() * time_proportion
    time_diff_proportion = end_point['TimeDifference'].item() * time_proportion
    speed = distance_proportion / time_diff_proportion
    distance = time_diff * speed
    cum_time_diff = int(start_point['CumTimeDiff'].item() + time_diff_proportion)
    # date = datetime.strptime(start_point['Date'].item(), '%Y-%m-%d %H:%M:%S') + dt.timedelta(seconds=int(
    # time_diff_proportion))
    date = pd.to_datetime(start_point['Date'].astype(str), format='%Y-%m-%d %H:%M:%S') + dt.timedelta(
        seconds=int(time_diff_proportion))
    altitude = (end_point['Altitude'].item() + start_point['Altitude'].item()) / 2
    name = start_point['CodeRoute'].item()

    geo_start = geopy.Point(start_point['Latitude'].item(), start_point['Longitude'].item())
    geo_end = geopy.Point(end_point['Latitude'].item(), end_point['Longitude'].item())
    middle_point = get_coordinates(geo_start, geo_end, distance_proportion)

    df_middle_point = ([[name, middle_point.latitude, middle_point.longitude, altitude,
                         date, speed, int(time_diff), distance, None, cum_time_diff]])

    return df_middle_point


def rgb(value, minimum, maximum):
    """
    Calculates an rgb color of a value depending of
    the minimum and maximum values.

    Parameters
    ----------
    value: float or int
    minimum: float or int
    maximum: float or int

    Returns
    -------
    rgb: tuple
    """
    value = float(value)
    minimum = float(minimum)
    maximum = float(maximum)

    if minimum == maximum:
        ratio = 0
    else:
        ratio = 2 * (value - minimum) / (maximum - minimum)

    b = int(max(0, 255 * (1 - ratio)))
    r = int(max(0, 255 * (ratio - 1)))
    g = 255 - b - r

    return r / 255.0, g / 255.0, b / 255.0


def calculateCumTimeDiff(df):
    """
    Calculates the cumulative of the time difference
    between points for each track of 'dfTrack'.
    """
    warnings.warn("The calculateCumTimeDiff function is deprecated and "
                  "will be removed in version 2.0.0. "
                  "Use the calculate_cum_time_diff function instead.",
                  FutureWarning,
                  stacklevel=8
                  )
    return calculate_cum_time_diff(df)


def calculate_cum_time_diff(df):
    """
    Calculates the cumulative of the time difference
    between points for each track of 'dfTrack'.
    """
    df = df.copy()

    df_cum = pd.DataFrame()
    grouped = df['CodeRoute'].unique()

    for name in grouped:
        df_slice = df[df['CodeRoute'] == name]
        df_slice = df_slice.reset_index(drop=True)
        df_slice['CumTimeDiff'] = df_slice['TimeDifference'].cumsum()
        df_cum = pd.concat([df_cum, df_slice])

    df_cum = df_cum.reset_index(drop=True)

    return df_cum


def isTimeFormat(time):
    """
    Check if 'time' variable has the format of one
    of the 'time_formats'
    """
    warnings.warn("The isTimeFormat function is deprecated and "
                  "will be removed in version 2.0.0. "
                  "Use the is_time_format function instead.",
                  FutureWarning,
                  stacklevel=8
                  )
    return is_time_format(time)


def is_time_format(time):
    """
    Check if 'time' variable has the format of one
    of the 'time_formats'
    """
    if time is None:
        return False

    for time_format in TIME_FORMATS:
        try:
            datetime.strptime(time, time_format)
            return True
        except ValueError:
            pass

    return False