#!/usr/bin/env python
# -*- coding: utf-8
"""Entry point to the server interface.

This takes care of the multiuser server bottle calls."""

import os
import sys
import json
import argparse

from multiprocessing import Process
from bottle import route, static_file, redirect, request, BaseRequest, response, hook, HTTPResponse
from bottle import run as run_server

import anvio
import anvio.utils as utils
import anvio.terminal as terminal
import anvio.bottleroutes as routes

from anvio.errors import ConfigError, FilesNPathsError, DictIOError, SamplesError

import anvio.usermgmt as usermgmt
import anvio.bottleroutes_multi as multiroutes
import copy

__author__ = "Tobias Paczian"
__copyright__ = "Copyright 2015, The anvio Project"
__credits__ = []
__license__ = "GPL 3.0"
__version__ = anvio.__version__
__maintainer__ = "Tobias Paczian"
__email__ = "tobiaspaczian@googlemail.com"
__status__ = "Development"


run = terminal.Run()
progress = terminal.Progress()

# get the absolute path for static directory under anvio
static_dir = os.path.join(os.path.dirname(utils.__file__), 'data/interactive')

# setup the command line user interface
parser = argparse.ArgumentParser(description="Start a stand-alone anvi'o visualization server")


groupA = parser.add_argument_group('USERS CONFIGURATION', "Information on where to find users data.")
groupB = parser.add_argument_group('HTTP SERVER CONFIGURATION', "Anvi'o HTTP server configuration parameters.")
groupC = parser.add_argument_group('SMTP SERVER CONFIGURATION', "How should the server send e-mails?")

groupA.add_argument(*anvio.A('users-data-dir'), **anvio.K('users-data-dir', {'required': True}))
groupA.add_argument(*anvio.A('dry-run'), **anvio.K('dry-run'))
groupB.add_argument(*anvio.A('ip-address'), **anvio.K('ip-address'))
groupB.add_argument(*anvio.A('port-number'), **anvio.K('port-number', {'default': None}))
groupB.add_argument(*anvio.A('hostname'), **anvio.K('hostname', {'help': "Hostname for the anvi'server. This will be\
                                                     used generate links back to the server in confirmation e-mails. For\
                                                     instance, if the hostname is 'HOSTNAME', links to the server will be\
                                                     built as 'http://HOSTNAME/'. If you don't declare a hostname, the IP\
                                                     address will be used for this purpose, and things will go South if the\
                                                     anvi'server is served along with multiple web services from one IP\
                                                     address.", 'default': None}))
groupC.add_argument(*anvio.A('smtp-config-file'), **anvio.K('smtp-config-file'))
groupC.add_argument(*anvio.A('validate-users-automatically'), **anvio.K('validate-users-automatically'))

args = parser.parse_args()

# setup defaults
args.manual_mode = True

port = args.port_number
ip = args.ip_address

# only get a port if it was not passed as a parameter
if not port:
    port = utils.get_available_port_num(start=8080, ip=ip)

if not port:
    run.info_single("Anvi'o failed to find a port number that is available :(", mc='red', nl_after = 1)
    sys.exit(-1)

if utils.is_port_in_use(port):
    run.info_single('It seems another application is already using the port number %d :/' % port, mc='red', nl_after = 1)
    sys.exit(-1)

try:
    if args.smtp_config_file:
        mailer = utils.Mailer(run = run, progress = progress)
        mailer.init_from_config(args.smtp_config_file)
        mailer.test()
    else:
        run.warning("You are running the server without any SMTP support. Some things\
                     are clearly not going to work (i.e., the serrver's ability to send\
                     confirmation e-mails), but all other funcitons should be alright!")
        mailer = None

    # initialize usermanagement
    userdb = usermgmt.UserMGMT(copy.deepcopy(args), anvio.__users_db_version__, mailer = mailer)
except ConfigError, e:
    print e
    sys.exit(-1)
except FilesNPathsError, e:
    print e
    sys.exit(-2)
except DictIOError, e:
    print e
    sys.exit(-3)
except SamplesError, e:
    print e
    sys.exit(-4)


#######################################################################################################################
# bottle callbacks start
#######################################################################################################################

def set_default_headers(response):
    response.set_header('Content-Type', 'application/json')
    response.set_header('Pragma', 'no-cache')
    response.set_header('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
    response.set_header('Expires', 'Thu, 01 Dec 1994 16:00:00 GMT')

@hook('before_request')
def validate_request():
    if request.path == '/':
        pass
    elif request.path == '/favicon.ico':
        pass
    elif request.path.startswith('/app/'):
        pass
    elif request.path.startswith('/public/'):
        pass
    elif request.path.startswith('/private/'):
        pass
    elif request.path.startswith('/confirm'):
        pass
    else:
        if ('X-Requested-With' not in request.headers) or (request.headers['X-Requested-With'] != 'XMLHttpRequest'):
            raise HTTPResponse(status=500, body='This API endpoint is only available via XMLHTTPRequest.')
    
@route('/')
def redirect_to_app():
    redirect('/app/home.html')

@route('/version')
def server_version():
    return multiroutes.server_version(request, userdb, response)

@route('/user', method='DELETE')
def delete_user():
    return multiroutes.delete_user(request, userdb, response)

@route('/clearance', method='POST')
def change_clearance():
    return multiroutes.change_clearance(request, userdb, response)
    
@route('/impersonate', method='POST')
def impersonate():
    return multiroutes.impersonate(request, userdb, response)

@route('/token', method='POST')
def get_user_by_token():
    return multiroutes.get_user_by_token(request, userdb, response)

@route('/requestAccount', method='POST')
def request_account():
    return multiroutes.request_account(request, userdb, response)

@route('/resetPassword', method='POST')
def reset_password():
    return multiroutes.reset_password(request, userdb, response)

@route('/checkAvailability', method='POST')
def check_availability():
    return multiroutes.check_availability(request, userdb, response)

@route('/changePassword', method='POST')
def change_password():
    return multiroutes.change_password(request, userdb, response)

@route('/confirm', method='GET')
def accept_user():
    return multiroutes.accept_user(request, userdb, response)

@route('/login', method='POST')
def login_to_app():
    return multiroutes.login_to_app(request, userdb, response)

@route('/logout', method='POST')
def logout_from_app():
    return multiroutes.logout_from_app(request, userdb, response)

@route('/private/<login>/<project>', method='GET')
def set_view_cookie_private(login, project):
    return multiroutes.set_view_cookie(request, userdb, response, login, project, True)

@route('/public/<login>/<project>', method='GET')
def set_view_cookie_public(login, project):
    return multiroutes.set_view_cookie(request, userdb, response, login, project, False)

@route('/project', method='POST')
def set_project():
    return multiroutes.set_project(request, userdb, response)

@route('/project', method='GET')
def get_current_project():
    return multiroutes.get_current_project(request, userdb, response)

@route('/project', method='PUT')
def update_project():
    return multiroutes.update_project(request, userdb, response)

@route('/project', method='DELETE')
def delete_project():
    return multiroutes.delete_project(request, userdb, response)

@route('/projectfiles', method='GET')
def get_current_project_files():
    return multiroutes.get_current_project_files(request, userdb, response)

@route('/share', method='POST')
def share_project():
    return multiroutes.share_project(request, userdb, response)

@route('/share', method='DELETE')
def delete_share():
    return multiroutes.delete_share(request, userdb, response)
    
@route('/upload', method='POST')
def receive_upload_file():
    return multiroutes.receive_upload_file(request, userdb, response)

@route('/uploadMore', method='POST')
def receive_additional_upload_file():
    return multiroutes.receive_additional_upload_file(request, userdb, response)

@route('/adminData', method='GET')
def admin_data():
    return multiroutes.admin_data(request, userdb, response)

@route('/adminProjectData', method='GET')
def admin_project_data():
    return multiroutes.admin_project_data(request, userdb, response)

@route('/adminProjectDetails', method='GET')
def admin_project_details():
    return multiroutes.admin_project_details(request, userdb, response)

@route('/app/:filename#.*#')
def send_static(filename):
    set_default_headers(response)
    return static_file(filename, root=static_dir)

@route('/app/downloadProjectFiles', method='GET')
def download_project_files():
    return multiroutes.get_current_project_archive(request, userdb, response)

@route('/public/<login>/:filename#.*#')
def send_static_public(login, filename):
    set_default_headers(response)
    return static_file(filename, root=static_dir)

@route('/private/<login>/:filename#.*#')
def send_static_private(login, filename):
    set_default_headers(response)
    return static_file(filename, root=static_dir)

@route('/data/<name>')
def send_data(name):
    set_default_headers(response)

    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    
    d = retval["data"]["d"]
    args = retval["data"]["args"]

    if name == "init":
        available_views = dict(zip(d.views.keys(), d.views.keys()))
        split_lengths = dict([tuple((c, d.splits_basic_info[c]['length']),) for c in d.splits_basic_info])
        return json.dumps( { "title": d.title,
                             "clusterings": (d.p_meta['default_clustering'], d.p_meta['clusterings']),
                             "views": (d.default_view, available_views),
                             "contigLengths": split_lengths,
                             "defaultView": d.views[d.default_view],
                             "mode": "server",
                             "readOnly": args.read_only,
                             "binPrefix": "Bin_",
                             "sessionId": 1,
                             "samplesOrder": d.samples_order_dict,
                             "sampleInformation": d.samples_information_dict,
                             "sampleInformationDefaultLayerOrder": d.samples_information_default_layer_order,
                             "stateAutoload": d.state,
                             "noPing": True,
                             "inspectionAvailable": d.auxiliary_profile_data_available,
                             "sequencesAvailable": True if d.split_sequences else False})

@route('/data/view/<view_id>')
def get_view_data(view_id):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_view_data(retval["data"]["args"], retval["data"]["d"], request, response, view_id)

@route('/tree/<tree_id>')
def get_tree(tree_id):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_tree(retval["data"]["args"], retval["data"]["d"], request, response, tree_id)

@route('/data/charts/<split_name>')
def charts(split_name):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.charts(retval["data"]["d"], split_name)

state_for_charts = {}

@route('/data/charts/set_state', method='POST')
def set_state():
    statekey = 'anon'
    retval = multiroutes.get_user(request, userdb, response)
    if retval[0]:
        statekey = retval[1]['login']
        
    global state_for_charts
    state_for_charts[statekey] = request.forms.get('state')

@route('/data/charts/get_state')
def get_parent_state():
    set_default_headers(response)
    statekey = 'anon'
    retval = multiroutes.get_user(request, userdb, response)
    if retval[0]:
        statekey = retval[1]['login']
        
    return state_for_charts[statekey]

@route('/data/gene/<gene_callers_id>')
def get_sequence_for_gene_call(gene_callers_id):
    return routes.get_sequence_for_gene_call(args, d, request, response, gene_callers_id)

@route('/data/contig/<split_name>')
def get_sequence_for_split(split_name):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_sequence_for_split(retval["data"]["args"], retval["data"]["d"], request, response, split_name)

@route('/data/collections')
def collections():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_collections(retval["data"]["args"], retval["data"]["d"], request, response)

@route('/data/collection/<collection_source>')
def get_collection_dict(collection_source):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_collection_dict(retval["data"]["args"], retval["data"]["d"], request, response, collection_source)

@route('/summary/<collection_name>/:filename#.*#')
def send_summary_static(collection_name, filename):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.send_summary_static(retval["data"]["args"], retval["data"]["d"], request, response, collection_name, filename)

@route('/summarize/<collection_name>')
def gen_summary(collection_name):
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.gen_summary(retval["data"]["args"], retval["data"]["d"], request, response, collection_name)

@route('/store_collection', method='POST')
def store_collections_dict():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.store_collections_dict(retval["data"]["args"], retval["data"]["d"], request, response)

@route('/data/completeness', method='POST')
def completeness():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.completeness(retval["data"]["d"], request)

@route('/state/autoload')
def state_autoload():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.state_autoload(retval["data"]["d"], response)

@route('/state/all')
def state_all():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.state_all(retval["data"]["d"], response)

@route('/state/get', method='POST')
def get_state():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.get_state(retval["data"]["d"], request, response)

@route('/state/save', method='POST')
def save_state():
    retval = userdb.set_user_data(request)
    if retval["status"] == "error":
        return json.dumps(retval)
    else:
        return routes.save_state(retval["data"]["args"], retval["data"]["d"], request, response)

#######################################################################################################################
# bottle callbacks end
#######################################################################################################################

# increase maximum size of form data to 100 MB
BaseRequest.MEMFILE_MAX = 1024 * 1024 * 100 

if args.dry_run:
    run.info_single('Dry run, eh? Bye!', 'red', nl_before = 1, nl_after=1)
    sys.exit()

try:
    if os.getuid() and port < 1024:
        run.warning("Running the server through the port number %d requires root priviliges,\
                     which your user does not seem to have. This may result in permission\
                     issues. That being said, anvi'o does not know anything about your system\
                     configuraiton, so if you have set things up properly, please ignore this\
                     note and accept our apologies. Otherwise, be prepared for a failed attempt\
                     to get this server running :)" % port)

    server_process = Process(target=run_server, kwargs={'host': ip, 'port': port, 'quiet': True, 'server': 'cherrypy'})
    server_process.start()

    run.info_single("Anvi'o is serving on port %d." % port, 'green', nl_before = 1, nl_after=1)
    server_process.join()
except KeyboardInterrupt:
    run.warning("The server is being terminated.", header='Please wait...')
    server_process.terminate()
    sys.exit(0)
