#!/usr/bin/env python

import os
import datetime
import sys
import json
import requests

from herepy.geocoder_api import GeocoderApi
from herepy.here_api import HEREApi
from herepy.utils import Utils
from herepy.error import HEREError
from herepy.models import RoutingResponse, RoutingMatrixResponse, RoutingResponseV8
from herepy.here_enum import (
    RouteMode,
    RoutingMode,
    RoutingTransportMode,
    RoutingMetric,
    RoutingApiReturnField,
    RoutingApiSpanField,
    MatrixSummaryAttribute,
    MatrixRoutingType,
    MatrixRoutingMode,
    MatrixRoutingProfile,
    MatrixRoutingTransportMode,
)
from herepy.objects import Avoid, Truck
from herepy import polling
from typing import List, Dict, Union, Optional


class RoutingApi(HEREApi):
    """A python interface into the HERE Routing API"""

    URL_CALCULATE_ROUTE = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json"
    URL_CALCULATE_ROUTE_V8 = "https://router.hereapi.com/v8/routes"
    URL_CALCULATE_MATRIX = "https://matrix.router.hereapi.com/v8/matrix"

    def __init__(self, api_key: str = None, timeout: int = None):
        """Returns a RoutingApi instance.
        Args:
          api_key (str):
            API key taken from HERE Developer Portal.
          timeout (int):
            Timeout limit for requests.
        """

        super(RoutingApi, self).__init__(api_key, timeout)

    def __get(
        self,
        base_url,
        data,
        key,
        response_cls,
        manipulation_key: str = None,
        keys_for_manipulation: List = None,
    ):
        url = Utils.build_url(base_url, extra_params=data)
        if manipulation_key and keys_for_manipulation:
            for k in keys_for_manipulation:
                url = url.replace(k, manipulation_key)
        response = requests.get(url, timeout=self._timeout)
        json_data = json.loads(response.content.decode("utf8"))
        if response.status_code == requests.codes.OK:
            if json_data.get(key) is not None:
                return response_cls.new_from_jsondict(json_data)
            else:
                raise error_from_routing_service_error(json_data)
        else:
            raise HEREError(
                    "Error occured on routing_api __get "
                    + sys._getframe(1).f_code.co_name
                    + " response status code "
                    + str(response.status_code)
                )

    @classmethod
    def __prepare_mode_values(cls, modes):
        mode_values = ""
        for mode in modes:
            mode_values += mode.__str__() + ";"
        mode_values = mode_values[:-1]
        return mode_values

    @classmethod
    def __list_to_waypoint(cls, waypoint_a):
        return str.format("geo!{0},{1}", waypoint_a[0], waypoint_a[1])

    def _route(self, waypoint_a, waypoint_b, modes=None, departure=None, arrival=None):
        if isinstance(waypoint_a, str):
            waypoint_a = self._get_coordinates_for_location_name(waypoint_a)
        if isinstance(waypoint_b, str):
            waypoint_b = self._get_coordinates_for_location_name(waypoint_b)
        data = {
            "waypoint0": self.__list_to_waypoint(waypoint_a),
            "waypoint1": self.__list_to_waypoint(waypoint_b),
            "mode": self.__prepare_mode_values(modes),
            "apikey": self._api_key,
        }
        if departure is not None and arrival is not None:
            raise HEREError("Specify either departure or arrival, not both.")
        if departure is not None:
            departure = self._convert_datetime_to_isoformat(departure)
            data["departure"] = departure
        if arrival is not None:
            arrival = self._convert_datetime_to_isoformat(arrival)
            data["arrival"] = arrival
        response = self.__get(
            self.URL_CALCULATE_ROUTE, data, "response", RoutingResponse
        )
        route = response.response["route"]
        maneuver = route[0]["leg"][0]["maneuver"]

        if any(mode in modes for mode in [RouteMode.car, RouteMode.truck]):
            # Get Route for Car and Truck
            response.route_short = self._get_route_from_vehicle_maneuver(maneuver)
        elif any(
            mode in modes
            for mode in [RouteMode.publicTransport, RouteMode.publicTransportTimeTable]
        ):
            # Get Route for Public Transport
            public_transport_line = route[0]["publicTransportLine"]
            response.route_short = self._get_route_from_public_transport_line(
                public_transport_line
            )
        elif any(mode in modes for mode in [RouteMode.pedestrian, RouteMode.bicycle]):
            # Get Route for Pedestrian and Biyclce
            response.route_short = self._get_route_from_non_vehicle_maneuver(maneuver)
        return response

    def bicycle_route(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Request a bicycle route between two points
        Args:
          waypoint_a:
            List contains latitude and longitude in order
            or string with the location name
          waypoint_b:
            List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.bicycle, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def car_route(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Request a driving route between two points
        Args:
          waypoint_a (List):
            List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.car, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def pedastrian_route(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Request a pedastrian route between two points
        Args:
          waypoint_a (List):
            List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.pedestrian, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def intermediate_route(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        waypoint_c: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Request a intermediate route from three points
        Args:
          waypoint_a (List):
            Starting List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            Intermediate List contains latitude and longitude in order
            or string with the location name.
          waypoint_c (List):
            Last List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.car, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def public_transport(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        combine_change: bool,
        modes: List[RouteMode] = None,
        departure="now",
    ) -> Optional[RoutingResponse]:
        """Request a public transport route between two points
        Args:
          waypoint_a (List):
            Starting List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            Intermediate List contains latitude and longitude in order
            or string with the location name.
          combine_change (bool):
            Enables the change manuever in the route response, which
            indicates a public transit line change.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.publicTransport, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def public_transport_timetable(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        combine_change: bool,
        modes: List[RouteMode] = None,
        departure: str = None,
        arrival: str = None,
    ) -> Optional[RoutingResponse]:
        """Request a public transport route between two points based on timetables
        Args:
          waypoint_a (List):
            Starting List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            Intermediate List contains latitude and longitude in order
            or string with the location name.
          combine_change (bool):
            Enables the change manuever in the route response, which
            indicates a public transit line change.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `None`.
          arrival (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `None`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.publicTransportTimeTable, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure, arrival)

    def location_near_motorway(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Calculates the fastest car route between two location
        Args:
          waypoint_a (List):
            List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.car, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def truck_route(
        self,
        waypoint_a: Union[List[float], str],
        waypoint_b: Union[List[float], str],
        modes: List[RouteMode] = None,
        departure: str = "now",
    ) -> Optional[RoutingResponse]:
        """Calculates the fastest truck route between two location
        Args:
          waypoint_a (List):
            List contains latitude and longitude in order
            or string with the location name.
          waypoint_b (List):
            List contains latitude and longitude in order
            or string with the location name.
          modes (List):
            List contains RouteMode enums.
          departure (str):
            Date time str in format `yyyy-mm-ddThh:mm:ss`. Default `now`.
        Returns:
          RoutingResponse
        Raises:
          HEREError"""

        if modes is None:
            modes = [RouteMode.truck, RouteMode.fastest]
        return self._route(waypoint_a, waypoint_b, modes, departure)

    def route_v8(
        self,
        transport_mode: RoutingTransportMode,
        origin: Union[List[float], str],
        destination: Union[List[float], str],
        via: Optional[List[List[float]]] = None,
        departure_time: Optional[str] = None,
        routing_mode: RoutingMode = RoutingMode.fast,
        alternatives: Optional[int] = None,
        avoid: Optional[Dict[str, List[str]]] = None,
        exclude: Optional[Dict[str, List[str]]] = None,
        units: Optional[RoutingMetric] = None,
        lang: Optional[str] = None,
        return_fields: List[RoutingApiReturnField] = [RoutingApiReturnField.polyline],
        span_fields: Optional[List[RoutingApiSpanField]] = None,
        truck: Optional[Dict[str, List[str]]] = None,
        scooter: Optional[Dict[str, str]] = None,
        headers: Optional[dict] = None,
    ) -> Optional[RoutingResponseV8]:
        """Calculates the route between given origin and destination.
        Args:
          transport_mode (RoutingTransportMode):
            Mode of transport to be used for the calculation of the route.
          origin (Union[List[float], str]):
            List contains latitude and longitude in order
            or string with the location name.
          destination (Union[List[float], str]):
            List contains latitude and longitude in order
            or string with the location name.
          via (Optional[List[List[float]]]):
            Locations defining via waypoints.
            Locations between origin and destination.
          departure_time (Optional[str]):
            Specifies the time of departure as defined by
            either date-time or full-date T partial-time in RFC 3339,
            section 5.6 (for example, 2019-06-24T01:23:45).
          routing_mode (RoutingMode):
            Specifies which optimization is applied during route calculation,
            fast as default value.
          alternatives (Optional[int]):
            Number of alternative routes to return aside from the optimal route.
          avoid (Optional[Dict[str, List[str]]]):
            Avoid routes that violate certain features of road network or
            that go through user-specified geographical bounding boxes.
            Sample use of parameter: `{"features": [controlledAccessHighway, tunnel]}`
          exclude (Optional[Dict[str, List[str]]]):
            Defines properties which will be strictly excluded from route calculation.
            Sample use of parameter:
            `{"countries": [A comma separated list of three-letter country codes (ISO-3166-1 alpha-3 code)]}`
          units (Optional[RoutingMetric]):
            Units of measurement used in guidance instructions. The default is metric.
          lang (Optional[str]):
            Default: "en-US"
            Specifies the preferred language of the response.
            The value should comply with the IETF BCP 47.
          return_fields (List[RoutingApiReturnField]):
            Defines which attributes are included in the response as part of data
            representation of a Route or Section.
          span_fields (Optional[List[RoutingApiSpanField]]):
            Defines which attributes are included in the response spans.
            For example, attributes,length will enable the fields attributes and length in the route response.
            This parameter also requires that the polyline option is set within the return parameter.
          truck (Optional[Dict[str, List[str]]]):
            Comma-separated list of shipped hazardous goods in the vehicle.
            Sample use of parameter: `{"shippedHazardousGoods": [explosive, gas, flammable]}`
          scooter (Optional[Dict[str, str]]):
            Scooter specific parameters.
            Sample use of parameter: `{"allowHighway": "true"}`
          headers (Optional[dict]):
            HTTP headers for requests.
            Sample:
            X-Request-ID
            User-provided token that can be used to trace a request or
            a group of requests sent to the service.
        Returns:
          RoutingResponseV8
        Raises:
          HEREError
        """

        if isinstance(origin, str):
            origin = self._get_coordinates_for_location_name(origin)
        if isinstance(destination, str):
            destination = self._get_coordinates_for_location_name(destination)
        data = {
            "transportMode": transport_mode.__str__(),
            "origin": str.format("{0},{1}", origin[0], origin[1]),
            "destination": str.format("{0},{1}", destination[0], destination[1]),
            "apiKey": self._api_key,
        }
        if via:
            via_keys = []
            for i, v in enumerate(via):
                key = str.format("{0}{1}", "via", i)
                via_keys.append(key)
                data[key] = str.format("{0},{1}", v[0], v[1])
        if departure_time:
            data["departureTime"] = departure_time
        data["routingMode"] = routing_mode.__str__()
        if alternatives:
            data["alternatives"] = alternatives
        if avoid:
            key = list(avoid.keys())[0]
            values = list(avoid.values())[0]
            data["avoid"] = {
                key: ",".join(values),
            }
        if exclude:
            key = list(avoid.keys())[0]
            values = list(avoid.values())[0]
            data["exclude"] = {
                key: ",".join(values),
            }
        if units:
            data["units"] = units.__str__()
        if lang:
            data["lang"] = lang
        if return_fields:
            data["return"] = ",".join([field.__str__() for field in return_fields])
        if span_fields:
            data["spans"] = ",".join([field.__str__() for field in span_fields])
        if truck:
            key = list(avoid.keys())[0]
            values = list(avoid.values())[0]
            data["truck"] = {
                key: ",".join(values),
            }
        if scooter:
            data["scooter"] = scooter

        response = self.__get(
            self.URL_CALCULATE_ROUTE_V8,
            data,
            "routes",
            RoutingResponseV8,
            manipulation_key="via",
            keys_for_manipulation=via_keys,
        )
        return response

    def __prepare_matrix_request_body(
        self,
        origins: Union[List[float], str],
        destinations: Union[List[float], str],
        matrix_type: MatrixRoutingType = MatrixRoutingType.world,
        center: Optional[List[float]] = None,
        radius: Optional[int] = None,
        profile: Optional[MatrixRoutingProfile] = None,
        departure: str = None,
        routing_mode: Optional[MatrixRoutingMode] = None,
        transport_mode: Optional[MatrixRoutingTransportMode] = None,
        avoid: Optional[Avoid] = None,
        truck: Optional[Truck] = None,
        matrix_attributes: Optional[List[MatrixSummaryAttribute]] = None,
    ) -> Dict:
        region_definition = {
            "type": matrix_type.__str__(),
        }
        if center:
            region_definition["center"] = {"lat": center[0], "lng": center[1]}
        if radius:
            region_definition["radius"] = radius
        request_body = {"regionDefinition": region_definition}

        if profile:
            request_body["profile"] = profile.__str__()
        if departure:
            request_body["departureTime"] = departure
        if routing_mode:
            request_body["routingMode"] = routing_mode.__str__()
        if transport_mode:
            request_body["transportMode"] = transport_mode.__str__()
        if matrix_attributes:
            request_body["matrixAttributes"] = [
                attribute.__str__() for attribute in matrix_attributes
            ]
        if avoid:
            request_body["avoid"] = {"features": avoid.features, "areas": avoid.areas}
        if truck:
            request_body["truck"] = {
                "shippedHazardousGoods": truck.shipped_hazardous_goods,
                "grossWeight": truck.gross_weight,
                "weightPerAxle": truck.weight_per_axle,
                "height": truck.height,
                "width": truck.width,
                "length": truck.length,
                "tunnelCategory": truck.tunnel_category,
                "axleCount": truck.axle_count,
                "type": truck.truck_type,
                "trailerCount": truck.trailer_count,
            }

        origin_list = []
        for i, origin in enumerate(origins):
            if isinstance(origin, str):
                origin_waypoint = self._get_coordinates_for_location_name(origin)
            else:
                origin_waypoint = origin
            lat_long = {"lat": origin_waypoint[0], "lng": origin_waypoint[1]}
            origin_list.append(lat_long)
        request_body["origins"] = origin_list

        destination_list = []
        for i, destination in enumerate(destinations):
            if isinstance(destination, str):
                destination_waypoint = self._get_coordinates_for_location_name(
                    destination
                )
            else:
                destination_waypoint = destination
            lat_long = {"lat": destination_waypoint[0], "lng": destination_waypoint[1]}
            destination_list.append(lat_long)
        request_body["destinations"] = destination_list

        return request_body

    def sync_matrix(
        self,
        origins: Union[List[float], str],
        destinations: Union[List[float], str],
        matrix_type: MatrixRoutingType = MatrixRoutingType.world,
        center: Optional[List[float]] = None,
        radius: Optional[int] = None,
        profile: Optional[MatrixRoutingProfile] = None,
        departure: str = None,
        routing_mode: Optional[MatrixRoutingMode] = None,
        transport_mode: Optional[MatrixRoutingTransportMode] = None,
        avoid: Optional[Avoid] = None,
        truck: Optional[Truck] = None,
        matrix_attributes: Optional[List[MatrixSummaryAttribute]] = None,
    ) -> Optional[RoutingMatrixResponse]:
        """Sync request a matrix of route summaries between M starts and N destinations.
        Args:
          origins (List):
            List of lists of coordinates [lat,long] of start waypoints.
            or list of string with the location names.
          destinations (List):
            List of lists of coordinates [lat,long] of destination waypoints.
            or list of string with the location names.
          matrix_type (MatrixRoutingType):
            Routing type used in definition of a region in which the matrix will be calculated.
          center (Optional[List]):
            Center of region definition, latitude and longitude.
          radius (Optional[int]):
            Center  of region definition.
          profile (Optional[MatrixRoutingProfile]):
            A profile ID enables the calculation of matrices with routes of arbitrary length.
          departure (str):
            time when travel is expected to start, e.g.: '2013-07-04T17:00:00+02'
          routing_mode (Optional[MatrixRoutingMode]):
            Route mode used in optimization of route calculation.
          transport_mode (Optional[MatrixRoutingTransportMode]):
            Depending on the transport mode special constraints, speed attributes and weights
            are taken into account during route calculation.
          avoid (Optional[Avoid]):
            Avoid routes that violate these properties.
          truck (Optional[Truck]):
            Different truck options to use during route calculation when transportMode = truck.
          matrix_attributes (List):
            List of MatrixSummaryAttribute enums.
        Returns:
          RoutingMatrixResponse
        Raises:
          HEREError: If an error is received from the server.
        """

        query_params = {
            "apiKey": self._api_key,
            "async": "false",
        }

        request_body = self.__prepare_matrix_request_body(
            origins=origins,
            destinations=destinations,
            matrix_type=matrix_type,
            center=center,
            radius=radius,
            profile=profile,
            departure=departure,
            routing_mode=routing_mode,
            transport_mode=transport_mode,
            avoid=avoid,
            truck=truck,
            matrix_attributes=matrix_attributes,
        )

        url = Utils.build_url(self.URL_CALCULATE_MATRIX, extra_params=query_params)
        headers = {"Content-Type": "application/json"}
        response = requests.post(
            url, json=request_body, headers=headers, timeout=self._timeout
        )
        json_data = json.loads(response.content.decode("utf8"))
        if response.status_code == requests.codes.OK:
            if json_data.get("matrix") is not None:
                return RoutingMatrixResponse.new_from_jsondict(json_data)
            else:
                raise HEREError(
                    "Error occured on routing_api sync_matrix "
                    + sys._getframe(1).f_code.co_name
                    + " response status code "
                    + str(response.status_code)
                )
        else:
            if "title" in json_data and "cause" in json_data:
                raise HEREError(
                    str.format(
                        "routing_api sync_matrix failed! title: {0}, cause: {1}",
                        json_data["title"],
                        json_data["cause"],
                    )
                )
            else:
                raise HEREError(
                    "Error occured on routing_api sync_matrix "
                    + sys._getframe(1).f_code.co_name
                )

    def __download_file(self, fileurl: str):
        filename = os.path.basename(fileurl)
        with requests.get(fileurl, stream=True) as r:
            r.raise_for_status()
            with open(filename, "wb") as f:
                for chunk in r.iter_content(chunk_size=8192):
                    f.write(chunk)
        print("{} file saved!".format(filename))
        return filename

    def __is_correct_response(self, response):
        status_code = response.status_code
        json_data = response.json()
        if json_data.get("matrix") is not None:
            return json_data
        elif json_data.get("status") is not None:
            print(
                "Matrix {} calculation {}".format(
                    json_data["matrixId"], json_data["status"]
                )
            )
            return False
        elif json_data.get("error") is not None and json_data.get("error_description"):
            raise HEREError(
                "Error occured on __is_correct_response: "
                + json_data["error"]
                + ", description: "
                + json_data["error_description"]
            )
        elif json_data.get("title") is not None and json_data.get("status"):
            raise HEREError(
                "Error occured on __is_correct_response: "
                + json_data["title"]
                + ", status: "
                + json_data["status"]
            )

    def async_matrix(
        self,
        token: str,
        origins: Union[List[float], str],
        destinations: Union[List[float], str],
        matrix_type: MatrixRoutingType = MatrixRoutingType.world,
        center: Optional[List[float]] = None,
        radius: Optional[int] = None,
        profile: Optional[MatrixRoutingProfile] = None,
        departure: str = None,
        routing_mode: Optional[MatrixRoutingMode] = None,
        transport_mode: Optional[MatrixRoutingTransportMode] = None,
        avoid: Optional[Avoid] = None,
        truck: Optional[Truck] = None,
        matrix_attributes: Optional[List[MatrixSummaryAttribute]] = None,
    ) -> Optional[RoutingMatrixResponse]:
        """Async request a matrix of route summaries between M starts and N destinations.
        Args:
          token (str):
            Bearer token required for async calls. This is the only working solution for now.
            How to create a bearer token:
            https://developer.here.com/documentation/identity-access-management/dev_guide/topics/sdk.html#step-1-register-your-application
            https://developer.here.com/documentation/identity-access-management/dev_guide/topics/postman.html
          origins (List):
            List of lists of coordinates [lat,long] of start waypoints.
            or list of string with the location names.
          destinations (List):
            List of lists of coordinates [lat,long] of destination waypoints.
            or list of string with the location names.
          matrix_type (MatrixRoutingType):
            Routing type used in definition of a region in which the matrix will be calculated.
          center (Optional[List]):
            Center of region definition, latitude and longitude.
          radius (Optional[int]):
            Center  of region definition.
          profile (Optional[MatrixRoutingProfile]):
            A profile ID enables the calculation of matrices with routes of arbitrary length.
          departure (str):
            time when travel is expected to start, e.g.: '2013-07-04T17:00:00+02'
          routing_mode (Optional[MatrixRoutingMode]):
            Route mode used in optimization of route calculation.
          transport_mode (Optional[MatrixRoutingTransportMode]):
            Depending on the transport mode special constraints, speed attributes and weights
            are taken into account during route calculation.
          avoid (Optional[Avoid]):
            Avoid routes that violate these properties.
          truck (Optional[Truck]):
            Different truck options to use during route calculation when transportMode = truck.
          matrix_attributes (List):
            List of MatrixSummaryAttribute enums.
        Returns:
          RoutingMatrixResponse.
        Raises:
          HEREError: If an error is received from the server.
        """

        query_params = {}

        request_body = self.__prepare_matrix_request_body(
            origins=origins,
            destinations=destinations,
            matrix_type=matrix_type,
            center=center,
            radius=radius,
            profile=profile,
            departure=departure,
            routing_mode=routing_mode,
            transport_mode=transport_mode,
            avoid=avoid,
            truck=truck,
            matrix_attributes=matrix_attributes,
        )

        url = Utils.build_url(self.URL_CALCULATE_MATRIX, extra_params=query_params)
        headers = {
            "Content-Type": "application/json",
            "Authorization": str.format("Bearer {0}", token),
        }
        json_data = json.dumps(request_body)
        response = requests.post(
            url, json=request_body, headers=headers, timeout=self._timeout
        )
        if response.status_code == requests.codes.ACCEPTED:
            json_data = response.json()
            print(
                "Matrix {} calculation {}".format(
                    json_data["matrixId"], json_data["status"]
                )
            )
            poll_url = json_data["statusUrl"]
            headers = {"Authorization": str.format("Bearer {0}", token)}
            print("Polling matrix calculation started!")
            result = polling.poll(
                lambda: requests.get(poll_url, headers=headers),
                check_success=self.__is_correct_response,
                step=5,
                poll_forever=True,
            )
            print("Polling matrix calculation completed!")
            poll_data = result.json()
            return RoutingMatrixResponse.new_from_jsondict(poll_data)
        else:
            json_data = response.json()
            if (
                json_data.get("error") is not None
                and json_data.get("error_description") is not None
            ):
                raise HEREError(
                    "Error occured on async_matrix: "
                    + json_data["error"]
                    + ", description: "
                    + json_data["error_description"]
                )
            elif (
                json_data.get("title") is not None
                and json_data.get("cause") is not None
            ):
                raise HEREError(
                    "Error occured on async_matrix: "
                    + json_data["title"]
                    + ", cause: "
                    + json_data["cause"]
                )
            else:
                raise HEREError(
                    "Error occured on async_matrix " + sys._getframe(1).f_code.co_name
                )

    def _get_coordinates_for_location_name(self, location_name: str) -> List[float]:
        """Use the Geocoder API to resolve a location name to a set of coordinates."""

        geocoder_api = GeocoderApi(self._api_key)
        try:
            geocoder_response = geocoder_api.free_form(location_name)
            coordinates = geocoder_response.items[0]["position"]
            return [coordinates["lat"], coordinates["lng"]]
        except (HEREError) as here_error:
            raise WaypointNotFoundError(here_error.message)

    @staticmethod
    def _convert_datetime_to_isoformat(datetime_object):
        """Convert a datetime.datetime object to an ISO8601 string."""

        if isinstance(datetime_object, datetime.datetime):
            datetime_object = datetime_object.isoformat()
        return datetime_object

    @staticmethod
    def _get_route_from_non_vehicle_maneuver(maneuver):
        """Extract a short route description from the maneuver instructions."""

        road_names = []

        for step in maneuver:
            instruction = step["instruction"]
            try:
                road_name = instruction.split('<span class="next-street">')[1].split(
                    "</span>"
                )[0]
                road_name = road_name.replace("(", "").replace(")", "")

                # Only add if it does not repeat
                if not road_names or road_names[-1] != road_name:
                    road_names.append(road_name)
            except IndexError:
                pass  # No street name found in this maneuver step
        route = "; ".join(list(map(str, road_names)))
        return route

    @staticmethod
    def _get_route_from_public_transport_line(public_transport_line_segment):
        """Extract a short route description from the public transport lines."""

        lines = []
        for line_info in public_transport_line_segment:
            lines.append(line_info["lineName"] + " - " + line_info["destination"])

        route = "; ".join(list(map(str, lines)))
        return route

    @staticmethod
    def _get_route_from_vehicle_maneuver(maneuver):
        """Extract a short route description from the maneuver instructions."""

        road_names = []

        for step in maneuver:
            instruction = step["instruction"]
            try:
                road_number = instruction.split('<span class="number">')[1].split(
                    "</span>"
                )[0]
                road_name = road_number.replace("(", "").replace(")", "")

                try:
                    street_name = instruction.split('<span class="next-street">')[
                        1
                    ].split("</span>")[0]
                    street_name = street_name.replace("(", "").replace(")", "")

                    road_name += " - " + street_name
                except IndexError:
                    pass  # No street name found in this maneuver step

                # Only add if it does not repeat
                if not road_names or road_names[-1] != road_name:
                    road_names.append(road_name)
            except IndexError:
                pass  # No road number found in this maneuver step

        route = "; ".join(list(map(str, road_names)))
        return route


class InvalidCredentialsError(HEREError):

    """Invalid Credentials Error Type.

    This error is returned if the specified token was invalid or no contract
    could be found for this token.
    """


class InvalidInputDataError(HEREError):

    """Invalid Input Data Error Type.

    This error is returned if the specified request parameters contain invalid
    data, such as due to wrong parameter syntax or invalid parameter
    combinations.
    """


class WaypointNotFoundError(HEREError):

    """Waypoint not found Error Type.

    This error indicates that one of the requested waypoints
    (start/end or via point) could not be found in the routing network.
    """


class NoRouteFoundError(HEREError):

    """No Route Found Error Type.

    This error indicates that no route could be constructed based on the input
    parameter.
    """


class LinkIdNotFoundError(HEREError):

    """Link Not Found Error Type.

    This error indicates that a link ID passed as input parameter could not be
    found in the underlying map data.
    """


class RouteNotReconstructedError(HEREError):

    """Route Not Reconstructed Error Type.

    This error indicates that the RouteId is invalid (RouteId can not be
    decoded into valid data) or route failed to be reconstructed from the
    RouteId. In every case a mitigation is to re-run CalculateRoute request to
    acquire a new proper RouteId.
    """


# pylint: disable=R0911
def error_from_routing_service_error(json_data):
    """Return the correct subclass for routing errors"""

    if "error" in json_data:
        if json_data["error"] == "Unauthorized":
            return InvalidCredentialsError(json_data["error_description"])

    if "subtype" in json_data:
        subtype = json_data["subtype"]
        details = json_data["details"]

        if subtype == "InvalidInputData":
            return InvalidInputDataError(details)
        if subtype == "WaypointNotFound":
            return WaypointNotFoundError(details)
        if subtype == "NoRouteFound":
            return NoRouteFoundError(details)
        if subtype == "LinkIdNotFound":
            return LinkIdNotFoundError(details)
        if subtype == "RouteNotReconstructed":
            return RouteNotReconstructedError(details)
    # pylint: disable=W0212
    return HEREError("Error occured on " + sys._getframe(1).f_code.co_name)
