# coding=utf-8

import optparse, os, math, sys, re

from flask import Flask, render_template, abort, url_for, redirect, send_from_directory, jsonify, request, g
from flask_babel import Babel
from werkzeug.contrib.profiler import ProfilerMiddleware
from datetime import timedelta
from dateutil.tz import tzlocal

from .staticVariables import TEMPLATE_DIR, STATIC_DIR, DATA_DIR, DEFAULT_STOP, DEFAULT_HOST, DEFAULT_PORT, APP_VERSION, AVAILABLE_LANGUAGES, TRANSLATION_DIR, GOOGLE_MAPS_KEY, GOOGLE_MAPS_TEST_KEY, GOOGLE_MAPS_REGION
from .dataHandler import loadJson, hasTemplate, hasData, isDirectory, CustomJsonEncoder
from .gtfs import GtfsHandler
from .locator import FoliLocator

app = Flask(__name__, template_folder = TEMPLATE_DIR, static_folder = STATIC_DIR)
app.json_encoder = CustomJsonEncoder
# pybabel is picky when extracting strings from jinja templates, to put it nicely,so this "extension" is used so pybabel can also use it
app.jinja_env.add_extension('server.templating.CustomFunctionsExtension')
app.config['BABEL_TRANSLATION_DIRECTORIES'] = TRANSLATION_DIR # undocumented Flask-Babel config option

# For localization Flask-BabelEx might be better for choice performance (they are api compatible, sans setting custom translation path)
# - Flask-Babel loads the translation catalog every request
babel = Babel(app)
gtfs = GtfsHandler('gtfs', dir = DATA_DIR, defer = (sys.path[0] == '' and __package__ != None))
locator = FoliLocator(endpointURI = 'http://data.foli.fi/siri/vm', userAgent = APP_VERSION, pollFreq = 3)

def getPaginationVariables(request, stopId, view, count, perPage, defArg = 0, argName = 'start'):
	vars = {}
	if count < perPage:
		return (0, perPage, count, vars)

	offset = request.args.get(argName, default = defArg, type = int)
	perPage = float(perPage) # convert to float for division

	if offset < perPage:
		offset = 0
	elif offset > count:
		offset = math.trunc(math.floor((count - 1) / perPage) * perPage)

	args = request.args.to_dict()
	if offset > 0:
		prevOffset = math.trunc(max(0, offset - perPage));
		if prevOffset > 0:
			args[argName] = prevOffset
		else:
			args.pop(argName, None)
		vars['prev_url'] = url_for('view', stopId = stopId, path = view, **args)

	if count > perPage and offset + perPage < count:
		nextOffset = math.trunc(min(offset + perPage, count))
		args[argName] = nextOffset
		vars['next_url'] = url_for('view', stopId = stopId, path = view, **args)

	if vars:
		vars['page_count'] = math.trunc(math.ceil(count / perPage))
		vars['on_page'] = math.trunc(math.floor(offset / perPage) + 1)

	return (offset, int(perPage), count, vars)

def getViewData(request, stopId, view):
	if view == 'timetables':
		offset, perPage, count, pagination = getPaginationVariables(request, stopId, view, gtfs.countDaysTrips(stopId), 15)
		return {
			'arrivals': gtfs.getNextTrips(stopId, limit = 5),
			'schedule': gtfs.getDaysTrips(stopId, offset = offset, limit = perPage, fullTrips = True),
			'pagination': pagination
		}
	elif view == 'map':
		return {
			'arrivals': gtfs.getNextTrips(stopId, limit = 2),
			'gmaps_key': GOOGLE_MAPS_KEY if not app.config['DEBUG'] else GOOGLE_MAPS_TEST_KEY,
			'gmaps_region': GOOGLE_MAPS_REGION 
		}

	return {}

@app.before_first_request
def firstRequestHandler():
	# start polling for realtime location data (naive, uses threading so GIL limited)
	locator.start()
	gtfs.setLocator(locator)

@app.before_request
def beforeRequestHandler():
	if not gtfs.isValid():
		abort(503, "Transit data not available...")

@app.after_request
def afterRequestHandler(response):
	response.headers['Server'] = APP_VERSION
	response.headers['X-Frame-Options'] = 'DENY'
	response.headers['X-XSS-Protection'] = '1; mode=block'
	response.headers['Content-Security-Policy'] = "default-src='self'"
	return response

@app.url_value_preprocessor
def pullUrlGlobals(endpoint, values):
	if endpoint == 'static' or values == None:
		return

	# stopId is esentially a global routing argument, its absence is an exception
	# these are snake_case because they also act as template variables by default
	g.stop_id = values.pop('stopId', None)

	# keep ref and lang query string arguments in outbuound urls, sanitized, for now retain language as query argument as opposed to path segment also
	g.referer =  values.pop('ref', request.args.get('ref', default = None, type = str))
	if not g.referer or not re.match("^[\w\d_-]+$", g.referer):
		g.referer = None
	else:
		# ref can include unique indentifier after the display keyword, for logging
		g.is_kiosk = g.referer.startswith('kiosk')

	g.lang_code = values.pop('lang', request.args.get('lang', default = None, type = str))
	if not g.lang_code or not re.match("^[\w\d_-]+$", g.lang_code):
		g.lang_code = getLocale()

@app.url_defaults
def urlDefaults(endpoint, values):
	if endpoint == 'static' or values == None:
		return

	# inject stop id if the endpoint is expecting it and it is not present
	if g.stop_id and not 'stopId' in values and app.url_map.is_endpoint_expecting(endpoint, 'stopId'):
		values['stopId'] = g.stop_id

	if g.referer and not 'ref' in values:
		values['ref'] = g.referer

	if g.lang_code and not 'lang' in values and g.lang_code != request.accept_languages.best_match(AVAILABLE_LANGUAGES):
		values['lang'] = g.lang_code

@babel.localeselector
def getLocale():
	lang = getattr(g, 'lang_code', None)
	if lang is not None and lang in AVAILABLE_LANGUAGES:
		return lang

	return request.accept_languages.best_match(AVAILABLE_LANGUAGES)

@babel.timezoneselector
def getTimezone():
	stopId = getattr(g, 'stop_id', None)
	if stopId is not None:
		return gtfs.getStopTimeZone(stopId)

	return tzlocal().tzname()

# if we don't have a stop code in URL redirect to DEFAULT_STOP for now
@app.route('/')
def index():
	return redirect(url_for('view', stopId = DEFAULT_STOP))

# avoid needless processing
@app.route('/favicon.ico')
@app.route('/robots.txt')
def sendRootFile():
	return send_from_directory(STATIC_DIR, request.path[1:])

# catch all route for any views, automatically loads data into template context
@app.route('/<stopId>/')
@app.route('/<stopId>/<path:path>')
def view(path = 'index.html'):
	# abort if attempting to access internal paths
	if path.startswith('internal/'):
		abort(403)

	path = path.rstrip('/')
	if isDirectory(path):
		path += '/index.html'

	if '.html' not in path:
		path = path + '.html'

	# abort if attempting to access invalid paths
	if not hasTemplate(path):
		abort(404)

	dataFile = path.replace('.html', '.json')
	data = None

	if hasData(g.stop_id + os.sep + dataFile):
		data = loadJson(g.stop_id + os.sep + dataFile)
	elif hasData(dataFile):
		data = loadJson(dataFile)

	if data == None:
		data = {}

	# snake_case because these are template variables
	data["stop_info"] = gtfs.getStop(g.stop_id)
	if data["stop_info"]:
		data.update(getViewData(request, g.stop_id, os.path.splitext(path)[0]))
		return render_template(path, **data)

	abort(404)

# URI: /api/v1/info/<stopId>/
@app.route('/api/v<int:version>/info/<stopId>/')
def apiStopInfo(version):
	if int(version) == 1:
		data = gtfs.getStop(g.stop_id)
		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# Get info on a specific bus: used in the map view when a bus is focused
# URI: /api/v1/bus/<busId>/
@app.route('/api/v<int:version>/bus/')
@app.route('/api/v<int:version>/bus/<busId>/')
@app.route('/api/v<int:version>/bus/<busId>/<getShape>/')
def apiBusInfo(version, busId = None, getShape = "false"):
	# busId is falsely optional here so that we can easily use url_for() for this endpoint in js
	if int(version) == 1 and busId != None:
		lastModified, data = locator.getLastResponse()
		if not data or data.get('status', None) != 'OK':
			return {}

		data = data['result']
		vehicles = data.get('vehicles', None)
		if not vehicles:
			return {}

		data = vehicles[busId]

		# add shapes to the data
		# route_short_name = lineref from data
		if (getShape == "true"):
			# FIXME: in theory this can contain shape information for multiple trips, since route_short_name doesn't have to be and isn't unique to one trip in GTFS
			data["shapes"] = gtfs.getTripShapes(data["lineref"])

		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# URI: /api/v1/arrivals/<stopId>/[?limit=<n>]
@app.route('/api/v<int:version>/arrivals/<stopId>/')
def apiNextTrips(version):
	if int(version) == 1:
		# see comments in gtfs.py about accuracy and potential use with SIRI SM endpoint
		data = gtfs.getNextTrips(g.stop_id, limit = request.args.get('limit', default = 5, type = int))

		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# URI: /api/v1/routes/<stopId>[/<type>]
@app.route('/api/v<int:version>/routes/<stopId>/')
@app.route('/api/v<int:version>/routes/<stopId>/<type>/')
def apiDayRoutes(version, type = 'full'):
	if int(version) == 1:
		asList = True if type == 'list' else False
		data = gtfs.getDaysRoutes(stopId = g.stop_id, asList = asList)

		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# URI: /api/v1/trips/<stopId>/[?offset=<n>&limit=<m>]
@app.route('/api/v<int:version>/trips/<stopId>/')
def apiDayTrips(version):
	if int(version) == 1:
		data = gtfs.getDaysTrips(
			stopId = g.stop_id,
			offset = request.args.get('offset', default = 0, type = int),
			limit = request.args.get('limit', default = -1, type = int)
		)

		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# URI: /api/v1/trip/<tripSlug>/
# tripSlug: stopId.index (get from trips or arrivals endpoint)
@app.route('/api/v<int:version>/trip/<stopId>.<int:index>/')
def apiStopTrip(version, index):
	if int(version) == 1:
		data = gtfs.getStopTrip(g.stop_id, index)
		if data:
			return jsonify(data)

		abort(404)

	abort(400)

# URI: /api/v1/locator[/<stopId>/<type>]
@app.route('/api/v<int:version>/locator/')
@app.route('/api/v<int:version>/locator/<stopId>/')
@app.route('/api/v<int:version>/locator/<stopId>/<type>/')
def apiLocatorService(version, type = 'routed'):
	if int(version) == 1:
		lastModified = data = None
		if g.stop_id == 'raw' or not g.stop_id:
			lastModified, data = locator.getLastResponse()
		elif type == 'routed':
			lastModified, data = gtfs.locateRoutedVehicles(g.stop_id)
		elif type == 'nearby':
			lastModified, data = gtfs.locateNearbyVehicles(g.stop_id, request.args.get('distance', default = 5000, type = int))

		if data:
			response = jsonify(data)
			response.last_modified = lastModified
			response.expires = lastModified + timedelta(seconds = 2)
			return response.make_conditional(request)

		abort(404)

	abort(400)

# URI: /api/v1/data/[<stopId>/]<path>.json
@app.route('/api/v<int:version>/data/<path:path>.json')
@app.route('/api/v<int:version>/data/<stopId>/<path:path>.json')
def apiDataFile(version, path):
	dataFile = path + '.json'

	# todo: use nginx sendfile when available
	if int(version) == 1:
		options = {
			'as_attachment': False,
			'mimetype': 'application/json'
		}

		# send_from_directory allows clientside caching by default, which is fine since these are static files
		if g.stop_id != None and hasData(g.stop_id + os.sep + dataFile):
			return send_from_directory(DATA_DIR, g.stop_id + os.sep + dataFile, **options)

		if hasData(dataFile):
			return send_from_directory(DATA_DIR, dataFile, **options)

		abort(404)

	abort(400)

def main():
	# parse hostname and port
	parser = optparse.OptionParser()
	parser.add_option("-H", "--host",
		help="Hostname of the server (default: %s)" % DEFAULT_HOST, default = DEFAULT_HOST)
	parser.add_option("-P", "--port",
		help="Port to listen to (default: %s)" % str(DEFAULT_PORT), default = str(DEFAULT_PORT))

	# hide debug from help
	parser.add_option("-d", "--debug",
		action="store_true", dest="debug", help=optparse.SUPPRESS_HELP, default = False)
	parser.add_option("-p", "--profile",
		action="store_true", dest="profile", help=optparse.SUPPRESS_HELP, default = False)

	(options, _) = parser.parse_args()

	if not gtfs.isValid():
		print(" * Loading GTFS feed, this may take a while...")
		gtfs.load('gtfs', dir = DATA_DIR)

	if options.debug and options.profile:
		app.config['PROFILE'] = True
		app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions = [30])

	app.run(
		debug = options.debug,
		host = options.host,
		port = int(options.port),
		use_reloader = False # unfortunately the reloader breaks when running with python -m 
	)
