#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
BikeMe for MeeGo Harmattan
Bike sharing stations on Nokia offline maps
Using CityBikes API (api.citybik.es)
"""

import sys
import os
import json
import subprocess
import threading

reload(sys)
sys.setdefaultencoding('utf-8')

from PySide.QtCore import QObject, QUrl, Slot, Signal, Property, QTimer
from PySide.QtGui import QApplication
from PySide.QtDeclarative import QDeclarativeView

HTTP_HELPER = "/opt/bikeme/http_helper.py"
CITYBIKES_API = "http://api.citybik.es/v2"
NEXTBIKE_API = "https://maps2.nextbike.net/maps/nextbike-live.json"

# Nextbike bike type mappings for WienMobil Rad
NEXTBIKE_BIKE_TYPES = {
    192: {"name": "Standard", "icon": "standard"},
    183: {"name": "E-Bike", "icon": "ebike"},
    348: {"name": "E-Bike", "icon": "ebike"},
    189: {"name": "Child Seat", "icon": "kids"},
    273: {"name": "Cargo Bike", "icon": "cargo"},
    143: {"name": "E-Cargo", "icon": "ecargo"},
    196: {"name": "Other", "icon": "standard"}
}

def nextbikeTypeInfo(bike):
    """Map one Nextbike bike entry to a display type.

    Nextbike reassigns the numeric bike_type now and then (the Vienna e-bike
    moved from 183 to 348), so an unknown id falls back to the battery fields,
    which only a pedelec carries.
    """
    bt = bike.get("bike_type", 192)
    if bt in NEXTBIKE_BIKE_TYPES:
        return NEXTBIKE_BIKE_TYPES[bt]
    if bike.get("pedelec_battery") is not None or bike.get("battery_pack"):
        return NEXTBIKE_BIKE_TYPES[348]
    return NEXTBIKE_BIKE_TYPES[192]


# Networks that should use Nextbike API instead of CityBikes
NEXTBIKE_NETWORKS = {
    "wienmobil-rad": 748,  # CityBikes ID -> Nextbike city_uid
}

class CityBikesManager(QObject):
    """Manager for CityBikes API requests"""
    
    networksUpdated = Signal()
    stationsUpdated = Signal()
    errorOccurred = Signal()
    loadingChanged = Signal()
    networksDataChanged = Signal()
    stationsDataChanged = Signal()
    errorMessageChanged = Signal()
    stationsFileWritten = Signal()
    
    def __init__(self, parent=None):
        super(CityBikesManager, self).__init__(parent)
        self._loading = False
        self._pending = {}
        self._networks_data = "[]"
        self._stations_data = "{}"
        self._error_message = ""
    
    def _get_loading(self):
        return self._loading
    
    def _set_loading(self, value):
        if self._loading != value:
            self._loading = value
            self.loadingChanged.emit()
    
    loading = Property(bool, _get_loading, notify=loadingChanged)
    
    def _get_networks_data(self):
        return self._networks_data
    
    networksData = Property(unicode, _get_networks_data, notify=networksDataChanged)
    
    def _get_stations_data(self):
        return self._stations_data
    
    stationsData = Property(unicode, _get_stations_data, notify=stationsDataChanged)
    
    def _get_error_message(self):
        return self._error_message
    
    errorMessage = Property(unicode, _get_error_message, notify=errorMessageChanged)
    
    @Slot(unicode)
    def writeStationsFile(self, jsonData):
        """Write stations data to /tmp/bikeme_stations.json"""
        try:
            import codecs
            filepath = "/tmp/bikeme_stations.json"
            with codecs.open(filepath, 'w', encoding='utf-8') as f:
                f.write(jsonData)
                f.flush()
                os.fsync(f.fileno())
            print u"Wrote stations to %s" % filepath
            self.stationsFileWritten.emit()
        except Exception as e:
            print u"Error writing stations file: %s" % e
            self._error_message = unicode(e)
            self.errorMessageChanged.emit()
            self.errorOccurred.emit()
    
    def _do_request(self, requestId, url):
        """Execute HTTP request in background thread"""
        try:
            cmd = [HTTP_HELPER, "GET", url, "", "{}"]
            print u"HTTP GET: %s" % url
            
            env = os.environ.copy()
            env['PYTHONIOENCODING'] = 'utf-8'
            
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
            out, err = proc.communicate()
            
            if isinstance(out, str):
                out = out.decode('utf-8')
            
            self._pending[requestId] = out
        except Exception as e:
            self._pending[requestId] = json.dumps({"error": unicode(e)})
    
    def _emit_if_ready(self, requestId, callback):
        """Check if request is done and emit result"""
        if requestId in self._pending:
            result = self._pending.pop(requestId)
            # Clear first: the callback may start a follow-up request (the
            # Nextbike -> CityBikes fallback), which turns loading back on.
            self._set_loading(False)
            callback(result)
            return True
        return False
    
    def _start_request(self, requestId, url, callback):
        """Start async HTTP request"""
        self._set_loading(True)
        thread = threading.Thread(target=self._do_request, args=(requestId, url))
        thread.daemon = True
        thread.start()
        
        def check():
            if not self._emit_if_ready(requestId, callback):
                QTimer.singleShot(100, check)
        QTimer.singleShot(100, check)
    
    @Slot()
    def getNetworks(self):
        """Fetch all available bike sharing networks"""
        url = CITYBIKES_API + "/networks"
        
        def on_result(data):
            try:
                parsed = json.loads(data)
                if "error" in parsed:
                    self._error_message = parsed["error"]
                    self.errorMessageChanged.emit()
                    self.errorOccurred.emit()
                    return
                
                networks = parsed.get("networks", [])
                # Sort by country, then city
                networks.sort(key=lambda x: (
                    x.get("location", {}).get("country", "ZZ"),
                    x.get("location", {}).get("city", "")
                ))
                
                self._networks_data = json.dumps(networks)
                self.networksDataChanged.emit()
                self.networksUpdated.emit()
                print u"Loaded %d networks" % len(networks)
            except Exception as e:
                self._error_message = unicode(e)
                self.errorMessageChanged.emit()
                self.errorOccurred.emit()
        
        self._start_request("networks", url, on_result)
    
    @Slot(unicode)
    def getStations(self, networkId):
        """Fetch stations for a specific network"""
        # Check if this network should use Nextbike API
        if str(networkId) in NEXTBIKE_NETWORKS:
            self._getNextbikeStations(networkId, NEXTBIKE_NETWORKS[str(networkId)])
            return
        
        self._getCityBikesStations(networkId)
    
    def _getCityBikesStations(self, networkId):
        """Fetch stations for a network from the CityBikes API"""
        # Request stations AND vehicles
        url = CITYBIKES_API + "/networks/" + networkId + "?fields=id,name,location,stations,vehicles"
        
        def on_result(data):
            try:
                parsed = json.loads(data)
                if "error" in parsed:
                    self._error_message = parsed["error"]
                    self.errorMessageChanged.emit()
                    self.errorOccurred.emit()
                    return
                
                network = parsed.get("network", {})
                stations = network.get("stations", [])
                vehicles = network.get("vehicles", [])
                
                # Add network info to response
                result = {
                    "network": {
                        "id": network.get("id", ""),
                        "name": network.get("name", ""),
                        "city": network.get("location", {}).get("city", ""),
                        "country": network.get("location", {}).get("country", "")
                    },
                    "stations": stations,
                    "vehicles": vehicles
                }
                
                self._stations_data = json.dumps(result)
                self.stationsDataChanged.emit()
                self.stationsUpdated.emit()
                print u"Loaded %d stations and %d vehicles for %s" % (len(stations), len(vehicles), networkId)
            except Exception as e:
                self._error_message = unicode(e)
                self.errorMessageChanged.emit()
                self.errorOccurred.emit()
        
        self._start_request("stations_" + networkId, url, on_result)
    
    def _getNextbikeStations(self, networkId, cityUid):
        """Fetch stations from Nextbike API with bike types"""
        url = NEXTBIKE_API + "?city=" + str(cityUid)
        
        def fall_back(reason):
            """Nextbike unusable -- serve the network from CityBikes instead.
            Loses the per-bike types, but the stations still load."""
            print u"Nextbike failed (%s), falling back to CityBikes" % reason
            self._getCityBikesStations(networkId)
        
        def on_result(data):
            try:
                parsed = json.loads(data)
                if "error" in parsed:
                    fall_back(parsed["error"])
                    return
                
                countries = parsed.get("countries", [])
                if not countries:
                    fall_back("no countries in response")
                    return
                
                country = countries[0]
                cities = country.get("cities", [])
                if not cities:
                    fall_back("no city data")
                    return
                
                city = cities[0]
                places = city.get("places", [])
                
                stations = []
                vehicles = []
                
                for place in places:
                    lat = place.get("lat", 0)
                    lng = place.get("lng", 0)
                    is_bike = place.get("bike", False)  # True = free-floating bike
                    
                    # Count bikes by type
                    bike_list = place.get("bike_list", [])
                    type_counts = {}
                    bike_uids = []
                    
                    for bike in bike_list:
                        type_info = nextbikeTypeInfo(bike)
                        type_name = type_info["name"]
                        type_counts[type_name] = type_counts.get(type_name, 0) + 1
                        bike_uids.append(bike.get("number", ""))
                    
                    if is_bike:
                        # Free-floating bike - add to vehicles
                        type_info = nextbikeTypeInfo(bike_list[0]) if bike_list else NEXTBIKE_BIKE_TYPES[192]
                        vehicles.append({
                            "id": place.get("uid", ""),
                            "latitude": lat,
                            "longitude": lng,
                            "extra": {
                                "uid": place.get("name", "").replace("BIKE ", ""),
                                "bike_type": type_info["name"],
                                "bike_type_icon": type_info["icon"]
                            }
                        })
                    else:
                        # Station
                        total_bikes = place.get("bikes", 0)
                        free_racks = place.get("free_racks", 0)
                        bike_racks = place.get("bike_racks", 0)
                        
                        # Build extra info with type counts
                        extra = {
                            "bike_uids": bike_uids,
                            "ebikes": type_counts.get("E-Bike", 0),
                            "normal_bikes": type_counts.get("Standard", 0),
                            "kids_bikes": type_counts.get("Child Seat", 0),
                            "cargo_bikes": type_counts.get("Cargo Bike", 0) + type_counts.get("E-Cargo", 0)
                        }
                        
                        stations.append({
                            "id": str(place.get("uid", "")),
                            "name": place.get("name", "Station"),
                            "latitude": lat,
                            "longitude": lng,
                            "free_bikes": total_bikes,
                            "empty_slots": free_racks if free_racks > 0 else max(0, bike_racks - total_bikes),
                            "extra": extra
                        })
                
                result = {
                    "network": {
                        "id": networkId,
                        "name": country.get("name", ""),
                        "city": city.get("name", ""),
                        "country": country.get("country", "")
                    },
                    "stations": stations,
                    "vehicles": vehicles
                }
                
                self._stations_data = json.dumps(result)
                self.stationsDataChanged.emit()
                self.stationsUpdated.emit()
                print u"Loaded %d stations and %d vehicles from Nextbike for %s" % (len(stations), len(vehicles), networkId)
            except Exception as e:
                import traceback
                traceback.print_exc()
                fall_back(unicode(e))
        
        self._start_request("nextbike_" + str(cityUid), url, on_result)


def main():
    print u"=== BikeMe 1.7.1 Starting ==="
    print u"Using CityBikes API: %s" % CITYBIKES_API
    print u"Using Nextbike API: %s" % NEXTBIKE_API
    
    app = QApplication(sys.argv)
    
    import_paths = [
        "/usr/lib/qt4/imports",
        "/usr/share/qt4/imports"
    ]
    
    manager = CityBikesManager()
    
    view = QDeclarativeView()
    view.setResizeMode(QDeclarativeView.SizeRootObjectToView)
    
    engine = view.engine()
    for path in import_paths:
        engine.addImportPath(path)
    
    view.rootContext().setContextProperty("cityBikesManager", manager)
    view.setSource(QUrl.fromLocalFile("/opt/bikeme/qml/main.qml"))
    
    if view.status() == QDeclarativeView.Error:
        for error in view.errors():
            print u"QML Error:", error.toString()
        return 1
    
    view.showFullScreen()
    
    return app.exec_()


if __name__ == "__main__":
    sys.exit(main())
