#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cgi
import os
import sys
import sqlite3
import urlparse
import datetime
import json
from email import utils
from os.path import join, dirname, exists

import bottle
from bottle import route, run, static_file, request, template, FormsDict, redirect, response, Bottle

URL_PREFIX = os.environ.get('URL_PREFIX', '')

ORIENTATIONS = (
    ('N', 'Nord'),
    ('NO', 'Nord-Ouest'),
    ('O', 'Ouest'),
    ('SO', 'Sud-Ouest'),
    ('S', 'Sud'),
    ('SE', 'Sud-Est'),
    ('E', 'Est'),
    ('NE', 'Nord-Est'),
)

# Angular sector for each direction, written as (start, stop) in degrees
ANGLES = {
     'N':  (-23, 22),
     'NO': (292, 337),
     'O':  (247, 292),
     'SO': (202, 247),
     'S':  (157, 202),
     'SE': (112, 157),
     'E':  (67, 112),
     'NE': (22, 67)
}

TABLE_NAME = 'contribs'
DB_FILENAME = join(dirname(__file__), 'db.sqlite3')
DB = sqlite3.connect(DB_FILENAME)

DB_COLS = (
('id', 'INTEGER PRIMARY KEY'),
('name', 'TEXT'),
('contrib_type', 'TEXT'),
('latitude', 'REAL'),
('longitude', 'REAL'),
('phone', 'TEXT'),
('email', 'TEXT'),
('access_type', 'TEXT'),
('connect_local', 'INTEGER'),
('connect_internet', 'INTEGER'),
('bandwidth', 'REAL'),
('share_part', 'REAL'),
('floor', 'INTEGER'),
('floor_total', 'INTEGER'),
('orientations', 'TEXT'),
('roof', 'INTEGER'),
('comment', 'TEXT'),
('privacy_name', 'INTEGER'),
('privacy_email', 'INTEGER'),
('privacy_coordinates', 'INTEGER'),
('privacy_place_details', 'INTEGER'),
('privacy_comment', 'INTEGER'),
('date', 'TEXT'),
)

GEOJSON_NAME = 'public.json'
GEOJSON_LICENSE_TYPE = 'ODC-BY-1.0'
GEOJSON_LICENSE_URL = 'http://opendatacommons.org/licenses/by/1.0/'

ANTISPAM_FIELD = 'url'

app = Bottle()

@app.route('/')
def home():
     redirect(urlparse.urljoin(request.path,join(URL_PREFIX, 'wifi-form')))

@app.route('/wifi-form')
def show_wifi_form():
    return template('wifi-form', errors=None, data = FormsDict(),
                    orientations=ORIENTATIONS, geojson=GEOJSON_NAME)

def create_tabble(db, name, columns):
    col_defs = ','.join(['{} {}'.format(*i) for i in columns])
    db.execute('CREATE TABLE {} ({})'.format(name, col_defs))

def escape(s):
     if not isinstance(s, (bool, float, int)) and (s != None):
          return cgi.escape(s)
     else:
          return s

def json_url(json_filename):
    """ Returns (relative) json URL with a querystring mentioning file mtime

    That's to prevent too much browser caching (mtime will change on file
    generation, changing querystring) while letting browser doing relevant
    caching.
    """
    file_path = join(dirname(__file__), 'json/', json_filename)
    mtime = os.path.getmtime(file_path)
    return '{}?mtime={}'.format(json_filename, mtime)

def save_to_db(db, dic):
    # SQLite is picky about encoding else
    tosave = {bytes(k):escape(v.decode('utf-8')) if isinstance(v,str)
              else escape(v)
              for k,v in dic.items()}
    tosave['date'] = utils.formatdate()
    return db.execute("""
INSERT INTO {}
(name, contrib_type, latitude, longitude, phone, email, access_type, connect_local, connect_internet, bandwidth, share_part, floor, floor_total, orientations, roof, comment,
privacy_name, privacy_email, privacy_place_details, privacy_coordinates, privacy_comment, date)
VALUES (:name, :contrib_type, :latitude, :longitude, :phone, :email, :access_type, :connect_local, :connect_internet, :bandwidth, :share_part, :floor, :floor_total, :orientations, :roof, :comment,
        :privacy_name, :privacy_email, :privacy_place_details, :privacy_coordinates, :privacy_comment, :date)
""".format(TABLE_NAME), tosave)

@app.route('/wifi-form', method='POST')
def submit_wifi_form():
    required = ('name', 'contrib-type',
                'latitude', 'longitude')
    required_or = (('email', 'phone'),)
    required_if = (
        ('contrib-type', 'share',('access-type', 'bandwidth',
                                    'share-part')),
    )

    field_names = {
        'name'        : 'Nom/Pseudo',
        'contrib-type': 'Type de participation',
        'latitude'    : 'Localisation',
        'longitude'   : 'Localisation',
        'phone'       : 'Téléphone',
        'email'       : 'Email',
        'access-type' : 'Type de connexion',
        'bandwidth'   : 'Bande passante',
        'share-part'  : 'Débit partagé',
        'floor' : 'Étage',
        'floor_total' : 'Nombre d\'étages total'
    }

    errors = []

    if request.forms.get(ANTISPAM_FIELD):
         errors.append(('', "Une erreur s'est produite"))

    for name in required:
        if (not request.forms.get(name)):
            errors.append((field_names[name], 'ce champ est requis'))

    for name_list in required_or:
        filleds = [True for name in name_list if request.forms.get(name)]
        if len(filleds) <= 0:
            errors.append((
                    ' ou '.join([field_names[i] for i in name_list]),
                    'au moins un des de ces champs est requis'))

    for key, value, fields  in required_if:
        if request.forms.get(key) == value:
            for name in fields:
                if not request.forms.get(name):
                    errors.append(
                        (field_names[name], 'ce champ est requis'))

    floor = request.forms.get('floor')
    floor_total = request.forms.get('floor_total')

    if floor and not floor_total:
        errors.append((field_names['floor_total'], "ce champ est requis"))
    if not floor and floor_total:
        errors.append((field_names['floor'], "ce champ est requis"))
    if floor and floor_total and (int(floor) > int(floor_total)):
        errors.append((field_names['floor'], "Étage supérieur au nombre total"))
    if floor and (int(floor) < 0):
        errors.append((field_names['floor'], "l'étage doit-être positif"))
    if floor_total and (int(floor_total) < 0):
        errors.append((field_names['floor_total'], "le nombre d'étages doit-être positif"))

    if errors:
        return template('wifi-form', errors=errors, data=request.forms,
                        orientations=ORIENTATIONS, geojson=json_url(GEOJSON_NAME))
    else:
        d = request.forms
        save_to_db(DB, {
                'name'         : d.get('name'),
                'contrib_type' : d.get('contrib-type'),
                'latitude'     : d.get('latitude'),
                'longitude'    : d.get('longitude'),
                'phone'        : d.get('phone'),
                'email'        : d.get('email'),
                'phone'        : d.get('phone'),
                'access_type'          : d.get('access-type'),
                'connect_local'        : 'local' in d.getall('connect-type'),
                'connect_internet'     : 'internet' in d.getall('connect-type'),
                'bandwidth'            : d.get('bandwidth'),
                'share_part'           : d.get('share-part'),
                'floor'                : d.get('floor'),
                'floor_total'                : d.get('floor_total'),
                'orientations'         : ','.join(d.getall('orientation')),
                'roof'         : d.get('roof'),
                'comment'              : d.get('comment'),
                'privacy_name'         : 'name' in d.getall('privacy'),
                'privacy_email'        : 'email' in d.getall('privacy'),
                'privacy_place_details': 'place_details' in d.getall('privacy'),
                'privacy_coordinates'  : 'coordinates' in d.getall('privacy'),
                'privacy_comment'      : 'comment' in d.getall('privacy'),
        })
        DB.commit()

        # Rebuild GeoJSON
        build_geojson()

        return redirect(urlparse.urljoin(request.path,join(URL_PREFIX,'thanks')))

@app.route('/thanks')
def wifi_form_thanks():
    return template('thanks')

@app.route('/assets/<filename:path>')
def send_asset(filename):
     for i in STATIC_DIRS:
          path = join(i, filename)
          if exists(path):
               return static_file(filename, root=i)
     raise bottle.HTTPError(404)

@app.route('/legal')
def legal():
    return template('legal')


"""
Results Map
"""

@app.route('/map')
def public_map():
    return template('map', geojson=json_url(GEOJSON_NAME))

@app.route('/public.json')
def public_geojson():
    return static_file('public.json', root=join(dirname(__file__), 'json/'))



"""
GeoJSON Functions
"""

# Useful for merging angle intervals (orientations)
def merge_intervals(l, wrap=360):
    """Merge a list of intervals, assuming the space is cyclic.  The
    intervals should already by sorted by start value."""
    if l == []:
        return []
    result = list()
    # Transform the 2-tuple into a 2-list to be able to modify it
    result.append(list(l[0]))
    for (start, stop) in l:
        current = result[-1]
        if start > current[1]:
            result.append([start, stop])
        else:
            result[-1][1] = max(result[-1][1], stop)
    if len(result) == 1:
        return result
    # Handle the cyclicity by merging the ends if necessary
    last = result[-1]
    first = result[0]
    if first[0] <= last[1] - wrap:
        result[-1][1] = max(result[-1][1], first[1] + wrap)
        result.pop(0)
    return result

def orientations_to_angle(orientations):
     """Return a list of (start, stop) angles from a list of orientations."""
     # Cleanup
     orientations = [o for o in orientations if o in ANGLES.keys()]
     # Hack to make leaflet-semicircle happy (drawing a full circle only
     # works with (0, 360))
     if len(orientations) == 8:
          return [[0, 360]]
     angles = [ANGLES[orientation] for orientation in orientations]
     angles.sort(key=lambda (x, y): x)
     return merge_intervals(angles)

# Save feature collection to a json file
def save_featurecollection_json(id, features, license=None):
    with open('json/' + id + '.json', 'w') as outfile:
        geojson = {
            "type" : "FeatureCollection",
            "features" : features,
            "id" : id,
        }
        if license:
            geojson['license'] = license
        json.dump(geojson, outfile)


# Build GeoJSON files from DB
def build_geojson():
    # Read from DB
    DB.row_factory = sqlite3.Row
    cur = DB.execute("""
        SELECT * FROM {} ORDER BY id DESC
        """.format(TABLE_NAME))

    public_features = []
    private_features = []

    # Loop through results
    rows = cur.fetchall()
    for row in rows:
        orientations = row['orientations'].split(',')
        if row['roof'] == "on":
             angles = [(0, 360)]
        else:
             angles = orientations_to_angle(orientations)
        # Private JSON file
        private_features.append({
            "type" : "Feature",
            "geometry" : {
                "type": "Point",
                 "coordinates": [row['longitude'], row['latitude']],
            },
             "id" : row['id'],
             "properties": {
                "name" : row['name'],
                "place" : {
                    'floor' : row['floor'],
                    'floor_total' : row['floor_total'],
                    'orientations' : orientations,
                    'angles' : angles,
                    'roof' : row['roof'],
                    'contrib_type' : row['contrib_type']
                },
                "comment" : row['comment']
             }
        })

        # Bypass non-public points
        if not row['privacy_coordinates']:
            continue

        # Public JSON file
        public_feature = {
            "type" : "Feature",
            "geometry" : {
                "type": "Point",
                 "coordinates": [row['longitude'], row['latitude']],
            },
             "id" : row['id'],
             "properties": {'contrib_type': row['contrib_type']}
        }

        # Add optionnal variables
        if row['privacy_name']:
            public_feature['properties']['name'] = row['name']

        if row['privacy_comment']:
            public_feature['properties']['comment'] = row['comment']

        if row['privacy_place_details']:
            public_feature['properties']['place'] = {
                'floor' : row['floor'],
                'floor_total' : row['floor_total'],
                'orientations' : orientations,
                'angles' : angles,
                'roof' : row['roof'],
            }

        # Add to public features list
        public_features.append(public_feature)

    # Build GeoJSON Feature Collection
    save_featurecollection_json('private', private_features)
    public_json_license = {
        "type" : GEOJSON_LICENSE_TYPE,
        "url" : GEOJSON_LICENSE_URL
    }
    save_featurecollection_json('public', public_features, public_json_license)



DEBUG = bool(os.environ.get('DEBUG', False))
LISTEN_ADDR= os.environ.get('BIND_ADDR', 'localhost')
LISTEN_PORT= int(os.environ.get('BIND_PORT', 8080))
URL_PREFIX = os.environ.get('URL_PREFIX', '').strip('/')
CUSTOMIZATION_DIR = os.environ.get('CUSTOMIZATION_DIR', None)
STATIC_DIRS = [join(dirname(__file__), 'assets')]


if __name__ == '__main__':
    if len(sys.argv) > 1:
        if sys.argv[1] == 'createdb':
            create_tabble(DB, TABLE_NAME, DB_COLS)
        if sys.argv[1] == 'buildgeojson':
            build_geojson()
    else:
        if URL_PREFIX:
            print('Using url prefix "{}"'.format(URL_PREFIX))
            root_app = Bottle()
            root_app.mount('/{}/'.format(URL_PREFIX), app)
            run(root_app, host=LISTEN_ADDR, port=LISTEN_PORT, reloader=DEBUG)

        if CUSTOMIZATION_DIR:
             custom_templates_dir = join(CUSTOMIZATION_DIR, 'views')
             if exists(custom_templates_dir):
                 bottle.TEMPLATE_PATH.insert(0, custom_templates_dir)
             custom_assets_dir = join(CUSTOMIZATION_DIR, 'assets')
             if exists(custom_assets_dir):
                 STATIC_DIRS.insert(0, custom_assets_dir)

        run(app, host=LISTEN_ADDR, port=LISTEN_PORT, reloader=DEBUG)
        DB.close()