from suds.client import Client
from suds.sax.element import Element
from suds import WebFault
from functools import partial
import logging
import os

log = logging.getLogger(__name__)
#TODO - timeouts and error handling
DARWIN_WEBSERVICE_NAMESPACE = ('com','http://thalesgroup.com/RTTI/2010-11-01/ldb/commontypes')

class DarwinLdbSession(object):
    """
    A connection to the Darwin LDB web service
    """

    def __init__(self, wsdl=None, api_key=None, timeout=5):
        """
        Constructor

        Keyword arguments:
        wsdl -- the URL of the Darwin LDB WSDL document. Will fall back to using the DARWIN_WEBSERVICE_WSDL environment variable if not supplied
        api_key -- a valid API key for the Darwin LDB webservice. Will fall back to the DARWIN_WEBSERVICE_API_KEY if not supplied
        timeout -- a timeout in seconds for calls to the LDB Webservice (default 5)
        """
        if not wsdl:
            wsdl = os.environ['DARWIN_WEBSERVICE_WSDL']
        if not api_key:
            api_key = os.environ['DARWIN_WEBSERVICE_API_KEY']
        self._soap_client = Client(wsdl)
        self._soap_client.set_options(timeout=timeout)
        #build soap headers
        token3 = Element('AccessToken', ns=DARWIN_WEBSERVICE_NAMESPACE)
        token_value = Element('TokenValue', ns=DARWIN_WEBSERVICE_NAMESPACE)
        token_value.setText(api_key)
        token3.append(token_value)
        self._soap_client.set_options(soapheaders=(token3))

    def _base_query(self):
        return self._soap_client.service['LDBServiceSoap']

    def get_station_board(self, crs, rows=10, include_departures=True, include_arrivals=False, destination_crs=None, origin_crs=None):
        """
        Query the darwin webservice to obtain a board for a particular station and return a StationBoard instance

        Positional arguments:
        crs -- the three letter CRS code of a UK station

        Keyword arguments:
        rows -- the number of rows to retrieve (default 10)
        include_departures -- include departing services in the departure board (default True)
        include_arrivals -- include arriving services in the departure board (default False)
        destination_crs -- filter results so they only include services calling at a particular destination (default None)
        origin_crs -- filter results so they only include services originating from a particular station (default None)
        """
        #Determine the darwn query we want to make
        if include_departures and include_arrivals:
            query_type = 'GetArrivalDepartureBoard'
        elif include_departures:
            query_type = 'GetDepartureBoard'
        elif include_arrivals:
            query_type = 'GetArrivalBoard'
        else:
            raise ValueError("get_station_board must have either include_departures or include_arrivals set to True")
        #build a query function
        q = partial(self._base_query()[query_type], crs=crs, numRows=rows)
        if destination_crs:
            if origin_crs:
                log.warn("Station board query can only filter on one of destination_crs and origin_crs, using only destination_crs")
            q = partial(q, filterCrs=destination_crs, filterType='to')
        elif origin_crs:
            q = partial(q, filterCrs=origin_crs, filterType='from')
        try:
            soap_response = q()
        except WebFault:
            raise WebServiceError
        return StationBoard(soap_response)

    
    def get_service_details(self, service_id):
        """
        Get the details of an individual service and return a ServiceDetails instance.

        Positional arguments:
        service_id: A Darwin LDB service id
        """
        try:
            soap_response = self._soap_client.service['LDBServiceSoap']['GetServiceDetails'](serviceID=service_id)
        except WebFault:
            raise WebServiceError
        return ServiceDetails(soap_response)

class SoapResponseBase(object):

    def __init__(self, soap_response):
        for dest_key, src_key in self.__class__.field_mapping:
            try:
                val = getattr(soap_response, src_key)
            except AttributeError:
                val = None
            setattr(self, '_' + dest_key, val)

        

class StationBoard(SoapResponseBase):
    """
    An abstract representation of a station departure board
    """

    field_mapping = [
        ('generated_at', 'generatedAt'),
        ('crs', 'crs'),
        ('location_name', 'locationName'),
    ]

    service_lists = [
        ('train_services', 'trainServices'),
        ('bus_services', 'busServices'),
        ('ferry_services', 'ferryServices')
    ]

    def __init__(self, soap_response, *args, **kwargs):
        super(StationBoard,self).__init__(soap_response, *args, **kwargs)
        #populate service lists - these are specific to station board objects, so not included in base class
        for dest_key, src_key in self.__class__.service_lists:
            try:
                service_rows = getattr(getattr(soap_response, src_key), 'service')
            except AttributeError:
                setattr(self, '_' + dest_key, [])
                continue

            setattr(self, '_' + dest_key, [ServiceItem(s) for s  in service_rows])
        #populate nrcc_messages
        if hasattr(soap_response, 'nrccMessages') and hasattr(soap_response.nrccMessages, 'message'):
            #TODO - would be nice to strip HTML from these, especially as it's not compliant with modern standards
            self._nrcc_messages = soap_response.nrccMessages.message
        else:
            self._nrcc_messages = []

    @property
    def generated_at(self):
        """
        The time at which the station board was generated. 
        """
        return self._generated_at

    @property
    def crs(self):
        """
        The CRS code for the station. 
        """
        return self._crs

    @property
    def location_name(self):
        """
        The name of the station.
        """
        return self._location_name

    @property
    def train_services(self):
        """
        A list of train services that appear on this board. Empty if there are none
        """
        return self._train_services

    @property
    def bus_services(self):
        """
        A list of bus services that appear on this board. Empty if there are none
        """
        return self._bus_services

    @property
    def ferry_services(self):
        """
        A list of ferry services that appear on this board. Empty if there are none
        """
        return self._ferry_services

    @property
    def nrcc_messages(self):
        """
        An optional list of important messages that should be displayed with the station board. Messages may include HTML hyperlinks and paragraphs.        """
        return self._nrcc_messages

    def __str__(self):
        return "%s - %s" % (self.crs, self.location_name)

class ServiceDetailsBase(SoapResponseBase):
    #The generic stuff that both service details classes have
    field_mapping = [
        ('sta', 'sta'),
        ('eta', 'eta'),
        ('std', 'std'),
        ('etd', 'etd'),
        ('platform', 'platform'),
        ('operator_name', 'operator'),
        ('operator_code', 'operatorCode'),
    ]


    @property
    def scheduled_arrival(self):
        raise NotImplementedError()

    @property
    def estimated_arrival(self):
        raise NotImplementedError()
        
    @property
    def scheduled_departure(self):
        raise NotImplementedError()

    @property
    def estimated_departure(self):
        raise NotImplementedError()

    @property
    def sta(self):
        """
        Scheduled Time of Arrival. This is optional and may be present for station boards which include arrivals.

        This is a human readable string rather than a proper datetime object and may not be a time at all
        """
        return self._sta

    @property
    def eta(self):
        """
        Estimated Time of Arrival. This is optional and may be present when an sta (Scheduled Time of Arrival) is available.

        This is a human readable string rather than a proper datetime object and may not be a time at all
        """
        return self._eta

    @property
    def std(self):
        """
        Scheduled Time of Departure. This is optional and may be present for station boards which include departures

        This is a human readable string rather than a proper datetime object and may not be a time at all
        """
        return self._std

    @property
    def etd(self):
        """
        Estimated Time of Departure. This is optional and may be present for results which contain an std (Scheduled Time of Departure)

        This is a human readable string rather than a proper datetime object and may not be a time at all
        """
        return self._etd

    @property
    def platform(self):
        """
        The platform number for the service at this station. Optional.
        """
        return self._platform

    @property
    def operator_name(self):
        """
        The name of the train operator
        """
        return self._operator_name

    @property
    def operator_code(self):
        """
        The National Rail abbreviation for the train operator
        """
        return self._operator_code

    #TODO -Adhoc alerts, datetime inflators - if possible

class ServiceItem(ServiceDetailsBase):
    """
    A single service from a bus, train or ferry departure/arrival board
    """

    field_mapping = ServiceDetailsBase.field_mapping + [
        ('is_circular_route', 'isCircularRoute'),
        ('service_id', 'serviceID'),
    ]
    
    def __init__(self, soap_data, *args, **kwargs):
        super(ServiceItem, self).__init__(soap_data, *args, **kwargs)

        #handle service location lists - these should be empty lists if there are no locations
        self._origins = [ServiceLocation(l) for l  in soap_data.origin.location] if hasattr(soap_data.origin, 'location') else []
        self._destinations = [ServiceLocation(l) for l  in soap_data.destination.location] if hasattr(soap_data.origin, 'location') else []

    @property
    def is_circular_route(self):
        """
        If True this service is following a circular route and will call again at this station.
        """
        return self._is_circular_route

    @property
    def service_id(self):
        """
        The unique ID of this service. This ID is specific to the Darwin LDB Service
        """
        return self._service_id

    @property
    def origins(self):
        """
        A list of ServiceLocation objects describing the origins of this service. A service may have more than multiple origins.
        """
        return self._origins

    @property
    def destinations(self):
        """
        A list of ServiceLocation objects describing the destinations of this service. A service may have more than multiple destinations.
        """
        return self._destinations

    @property
    def destination_text(self):
        """
        Human readable string describing the destination(s) of this service
        """
        return self._location_formatter(self.destinations)

    @property
    def origin_text(self):
        """
        Human readable string describing the origin(s) of this service
        """
        return self._location_formatter(self.origins)

    def _location_formatter(self, location_list):
        return ", ".join([str(l) for l in location_list])

    def __str__(self):
        return "Service %s" % (self.service_id)


class ServiceLocation(SoapResponseBase):
    """
    A single location from a service origin/destination list
    """
    field_mapping = [
        ('location_name', 'locationName'),
        ('crs', 'crs'),
        ('via', 'via'),
        ('future_change_to', 'futureChangeTo')
    ]
    
    @property
    def location_name(self):
        """
        Location name
        """
        return self._location_name

    @property
    def crs(self):
        """
        The CRS code of the location
        """
        return self._crs

    @property
    def via(self):
        """
        An optional string that should be displayed alongside the location_name. This provides additional context regarding an ambiguous route.
        """
        return self._via

    @property
    def future_change_to(self):
        """
        An optional string that indicates a service type (Bus/Ferry/Train) which will replace the current service type in the future.
        """
        return self._future_change_to
    
    def __str__(self):
        if self.via:
            return "%s %s" % (self.location_name, self.via)
        else:
            return self.location_name

class ServiceDetails(ServiceDetailsBase):
    """
    In depth details of a single service
    """

    field_mapping = ServiceDetailsBase.field_mapping + [
        ('is_cancelled', 'isCancelled'),
        ('disruption_reason', 'disruptionReason'),
        ('overdue_message', 'overdueMessage'),
        ('ata', 'ata'),
        ('atd', 'atd'),
    ]
   
    def __init__(self, soap_data, *args, **kwargs):
        super(ServiceDetails, self).__init__(soap_data, *args, **kwargs)
        #print soap_data.subsequentCallingPoints.callingPointList[0]
        self._previous_calling_points = self._calling_point_list(soap_data, 'previousCallingPoints')
        self._subsequent_calling_points = self._calling_point_list(soap_data, 'subsequentCallingPoints')

    def _calling_point_list(self, soap_data, src_key):
        try:
            calling_points = getattr(getattr(soap_data, src_key), 'callingPointList')
        except AttributeError:
            return []
        #print calling_points[0].callingPoint[0]
        points = []
        for sublist in calling_points:
            for point in sublist.callingPoint:
                points.append(CallingPoint(point))
        return points

    @property
    def is_cancelled(self):
        """
        True if this service is cancelled at this location.
        """
        return self._is_cancelled

    @property
    def disruption_reason(self):
        """
        A string containing a disruption reason for this service, if it is delayed or cancelled.
        """
        return self._disruption_reason

    @property
    def overdue_message(self):
        """
        A string that describes an overdue event
        """
        return self._overdue_message

    @property
    def ata(self):
        """
        Actual Time of Arrival.

        A human readable string, not guaranteed to be a machine-parsable time
        """
        return self._ata

    @property
    def atd(self):
        """
        Actual Time of Departure.

        A human readable string, not guaranteed to be a machine-parsable time
        """
        return self._atd

    @property
    def previous_calling_points(self):
        """
        A list of CallingPoint objects 
        """
        return self._previous_calling_points

    @property
    def subsequent_calling_points(self):
        """
        A list of CallingPoint objects 
        """
        return self._subsequent_calling_points

class CallingPoint(SoapResponseBase):
    """A single calling point on a train route"""
    field_mapping = [
        ('location_name', 'locationName'),
        ('crs', 'crs'),
        ('et', 'et'),
        ('at', 'at')
    ]

    @property
    def location_name(self):
        """
        Location name
        """
        return self._location_name

    @property
    def crs(self):
        """
        The CRS code for this location 
        """
        return self._crs

    @property
    def at(self):
        """
        Actual time

        Human readable string, no guaranteed format
        """
        return self._at

    @property
    def et(self):
        """
        Estimated time

        Human readable string, no guaranteed format
        """
        return self._et

class WebServiceError(Exception):
    pass
