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

The massage of the data is being taken care of in the interactive module,
and this file implements the bottle callbacks."""

import os
import sys
import json
import argparse
import webbrowser
import datetime
import random

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

import anvio
import anvio.dbops as dbops
import anvio.utils as utils
import anvio.dictio as dictio
import anvio.terminal as terminal
import anvio.summarizer as summarizer
import anvio.interactive as interactive

from anvio.errors import ConfigError, FilesNPathsError, DictIOError


__author__ = "Özcan Esen"
__copyright__ = "Copyright 2015, The anvio Project"
__credits__ = ["Doğan Can Kilment", "Gökmen Göksel", "Gökmen Görgen"]
__license__ = "GPL 3.0"
__version__ = anvio.__version__
__maintainer__ = "A. Murat Eren"
__email__ = "a.murat.eren@gmail.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')

parser = argparse.ArgumentParser(description="Start an anvi'o server for the interactive interface")
parser.add_argument('-p', '--profile-db', metavar = 'PROFILE_DB', default = None,
                    help = "Anvi'o profile database")
parser.add_argument('-a', '--annotation-db', metavar = 'ANNOTATION_DB', default = None,
                    help = 'Annotation database generated by "anvi-gen-annotation". Inclusion of\
                            this file will drastically increase the usefulness of the pipeline.')
parser.add_argument('-v', '--view', metavar = 'VIEW', default = None,
                    help = 'What view to show on the interface. To see a list of available views, use --show-views flag.')
parser.add_argument('-f', '--fasta-file', metavar = 'FASTA', default = None,
                    help = 'FASTA file for contigs.')
parser.add_argument('-m', '--metadata', metavar = 'TXT', default = None,
                    help = 'TAB-delimited metadata file')
parser.add_argument('-t', '--tree', metavar = 'NEWICK', default = None,
                    help = 'Newick tree of contigs. Declaring a tree file using this parameter will override\
                            the tree file in RUNINFO.')
parser.add_argument('--title', metavar = "TITLE", default = None,
                    help = "Title for the interface. If you are working with a RUNINFO dict, the title\
                            will be determined based on information stored in that file. Regardless,\
                            you can override that value using this parameter. If you are not using a\
                            anvio RUNINFO dictionary, a meaningful title will appear in the interface\
                            only if you define one using this parameter.")
parser.add_argument('-A', '--additional-metadata', metavar = "FILE", default = None,
                    help = "A TAB-delimited file for additional metadata for splits. The first column\
                            should be split names, and the rest of each column should be a metadata filed.\
                            The file does not need to contain all split names, or values for each split in\
                            every column. Anvi'o will deal with missing data nicely.")
parser.add_argument('-V', '--additional-view', metavar = "FILE", default = None,
                    help = "A TAB-delimited file for an additional view to be used in the interface. This\
                            file file should contain all split names, and values for each of them in all\
                            samples.")
parser.add_argument('-S', '--summary-index', metavar = "FILE", default = None,
                     help = "SUMMARY.cp, if there is one available, to inspect contigs from the interface. Will\
                             override the one found in RUNINFO file if it was also declared using -r parameter.")
parser.add_argument('-o', '--output-dir', metavar = 'DIRECTORY', default = None,
                    help = 'Output directory for output storage')
parser.add_argument('-P', '--port-number', metavar = 'INT', default = 8080, type=int,
                    help = 'Port number to use for communication; the default\
                            (%(default)d) should be OK for almost everyone.')
parser.add_argument('-I', '--ip-address', metavar = 'IP_ADDR', default = '0.0.0.0', type=str,
                    help = 'IP address for the HTTP server. The default ip address (%(default)s) should\
                            work just fine for most.')
parser.add_argument('--show-views', action = 'store_true', default = False,
                        help = 'When declared, the program will show a list of available views, and exit.')
parser.add_argument('--dry-run', action = 'store_true', default = False,
                        help = 'Do not start the server, do not fire up the browser.')
parser.add_argument('--skip-check-names', action = 'store_true', default = False,
                        help = 'For debugging purposes. If you are reading this, you should not use it.')
parser.add_argument('--split-hmm-layers', action = 'store_true', default = False,
                        help = 'When declared, this flag tells the interface to split every gene found in HMM\
                                searches that were performed against non-singlecopy gene HMM profiles into\
                                their own layer. Please see the documentation for details.')
parser.add_argument('--server-only', action = 'store_true', default = False,
                        help = 'The default behavior is to start the local server, and fire up a browser that\
                                connects to the server. If you have other plans, and want to start the server\
                                without calling the browser, this is the flag you need.')
parser.add_argument('--read-only', action = 'store_true', default = False,
                        help = 'When the interactive interface is started with this flag, all database "write"\
                                operations will be disabled.')


args = parser.parse_args()

port = args.port_number
ip = args.ip_address

port = utils.get_available_port_num(start = port, ip=ip)

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

unique_session_id = random.randint(0,9999999999)

try:
    d = interactive.InputHandler(args)
except ConfigError, e:
    print e
    sys.exit(-1)
except FilesNPathsError, e:
    print e
    sys.exit(-2)
except DictIOError, e:
    print e
    sys.exit(-3)


#######################################################################################################################
# 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')

@route('/')
def redirect_to_app():
    redirect('/app/index.html')

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

@route('/data/<name>')
def send_data(name):
    set_default_headers(response)
    if name == "clusterings":
        return json.dumps((d.p_meta['default_clustering'], d.p_meta['clusterings']), )
    elif name == "views":
        available_views = dict(zip(d.views.keys(), d.views.keys()))
        return json.dumps((d.default_view, available_views), )
    elif name == "default_view":
        return json.dumps(d.views[d.default_view])
    elif name == "contig_lengths":
        split_lengths = dict([tuple((c, d.splits_basic_info[c]['length']),) for c in d.splits_basic_info])
        return json.dumps(split_lengths)
    elif name == "title":
        return json.dumps(d.title)
    elif name == "mode":
        return json.dumps("full")
    elif name == "read_only":
        return json.dumps(args.read_only)
    elif name == "bin_prefix":
        return json.dumps("Bin_")
    elif name == "session_id":
        return json.dumps(unique_session_id)

@route('/data/view/<view_id>')
def get_view_data(view_id):
    return json.dumps(d.views[view_id])

@route('/tree/<tree_id>')
def send_tree(tree_id):
    set_default_headers(response)

    if tree_id in d.p_meta['clusterings']:
        run.info_single("Clustering of '%s' has been requested" % (tree_id))
        return json.dumps(d.p_meta['clusterings'][tree_id]['newick'])

@route('/data/charts/<split_name>')
def charts(split_name):
    data = {'layers': [],
             'index': None,
             'total': None,
             'coverage': [],
             'variability': [],
             'competing_nucleotides': [],
             'previous_contig_name': None,
             'next_contig_name': None,
             'genes': []}

    if not d.splits_summary_index:
        run.info_single("Contig details were requested, but no splits summary index is found.", mc = 'red')
        return None

    if not d.splits_summary_index.has_key(split_name):
        return data

    index_of_split = d.split_names_ordered.index(split_name)
    if index_of_split:
        data['previous_contig_name'] = d.split_names_ordered[index_of_split - 1]
    if (index_of_split + 1) < len(d.split_names_ordered):
        data['next_contig_name'] = d.split_names_ordered[index_of_split + 1]

    data['index'] = index_of_split + 1
    data['total'] = len(d.split_names_ordered)

    splits = dictio.read_serialized_object(d.P(d.splits_summary_index[split_name]))
    layers = sorted(splits.keys())

    for layer in layers:
        data['layers'].append(layer)
        data['coverage'].append(splits[layer]['coverage'])
        data['variability'].append(splits[layer]['variability'])
        data['competing_nucleotides'].append(splits[layer]['competing_nucleotides'])

    levels_occupied = {1: []}
    for entry_id in d.split_to_genes_in_splits_ids[split_name]:
        prot_id = d.genes_in_splits[entry_id]['prot']
        p = d.genes_in_splits[entry_id]
        # p looks like this at this point:
        #
        # {'percentage_in_split': 100,
        #  'start_in_split'     : 16049,
        #  'stop_in_split'      : 16633}
        #  'prot'               : u'prot2_03215',
        #  'split'              : u'D23-1contig18_split_00036'}
        #
        # we will add two more attributes:
        p['direction'] = d.genes_in_contigs_dict[prot_id]['direction']
        p['function'] = d.genes_in_contigs_dict[prot_id]['function'] or None

        for level in levels_occupied:
            level_ok = True
            for gene_tuple in levels_occupied[level]:
                if (p['start_in_split'] >= gene_tuple[0] - 100 and p['start_in_split'] <= gene_tuple[1] + 100) or\
                            (p['stop_in_split'] >= gene_tuple[0] - 100 and p['stop_in_split'] <= gene_tuple[1] + 100):
                    level_ok = False
                    break
            if level_ok:
                levels_occupied[level].append((p['start_in_split'], p['stop_in_split']), )
                p['level'] = level
                break
        if not level_ok:
            levels_occupied[level + 1] = [(p['start_in_split'], p['stop_in_split']), ]
            p['level'] = level + 1

        data['genes'].append(p)

    return json.dumps(data)

state_for_charts = {}

@route('/data/charts/set_state', method='POST')
def set_state():
    global state_for_charts
    state_for_charts = request.forms.get('state')

@route('/data/charts/get_state')
def get_parent_state():
    set_default_headers(response)
    return state_for_charts

@route('/data/contig/<split_name>')
def split_info(split_name):
    set_default_headers(response)
    return json.dumps(d.split_sequences[split_name])

@route('/data/collections')
def collections():
    csd = d.collections.sources_dict
    run.info_single('Collection sources has been requested (info dict with %d item(s) has been returned).' % len(csd), cut_after = None)
    set_default_headers(response)
    return json.dumps(csd)

@route('/data/collection/<collection_source>')
def get_collection_dict(collection_source):
    run.info_single('Data for collection source "%s" has been requested.' % len(collection_source))
    set_default_headers(response)
    return json.dumps({'data'  : d.collections.get_collection_dict(collection_source),
                       'colors': d.collections.get_collection_colors(collection_source)})

@route('/summary/<collection_id>/:filename#.*#')
def send_summary_static(collection_id, filename):
    set_default_headers(response)
    return static_file(filename, root=os.path.join(os.path.dirname(d.profile_db_path), 'SUMMARY_%s' % collection_id))

@route('/summarize/<collection_id>')
def gen_summary(collection_id):
    if args.read_only:
        return json.dumps("Sorry! This is a read-only instance.")

    run.info_single('A summary of collection "%s" has been requested.' % collection_id)
    set_default_headers(response)

    class Args:
        pass

    summarizer_args = Args()
    summarizer_args.profile_db = d.profile_db_path
    summarizer_args.annotation_db = d.annotation_db_path
    summarizer_args.collection_id = collection_id
    summarizer_args.list_collections = None
    summarizer_args.debug = None
    summarizer_args.output_directory = os.path.join(os.path.dirname(summarizer_args.profile_db), 'SUMMARY_%s' % collection_id)

    try:
        summary = summarizer.Summarizer(summarizer_args, r = run, p = progress)
        summary.process()
    except Exception as e:
        return json.dumps({'error': 'Something failed. This is what we know: %s' % e})

    run.info_single('HTML output for summary is ready: %s' % summary.index_html)
    
    url = "http://%s:%d/summary/%s/index.html" % (ip, port, collection_id)
    return json.dumps({'url': url})  


@route('/store_collection', method='POST')
def store_collections_dict():
    if args.read_only:
        return json.dumps("Sorry! This is a read-only instance.")

    source = request.forms.get('source')
    data = json.loads(request.forms.get('data'))
    colors = json.loads(request.forms.get('colors'))

    if not len(source):
        run.info_single('Lousy attempt from the user to store their collection under an empty source identifier name :/')
        return json.dumps("Error: Collection name cannot be empty.")

    num_splits = sum(len(l) for l in data.values())
    if not num_splits:
        run.info_single('The user to store 0 splits as a collection :/')
        return json.dumps("Error: There are no selections to store (you haven't selected anything).")

    if source in d.collections.sources_dict:
        e = d.collections.sources_dict[source]
        if e['read_only']:
            run.info_single('Lousy attempt from the user to store their collection under "%s" :/' % source)
            return json.dumps("Well, '%s' is a read-only collection, so you need to come up with a different name... Sorry!" % source)

    run.info_single('A request to store %d bins that describe %d splits under the collection id "%s"\
                     has been made.' % (len(data), num_splits, source), cut_after = None)

    collections = dbops.TablesForCollections(d.profile_db_path, anvio.__profile__version__)
    collections.append(source, data, colors)
    d.collections.populate_sources_dict(d.profile_db_path, anvio.__profile__version__)
    msg = "New collection '%s' with %d bin%s been stored." % (source, len(data), 's have' if len(data) > 1 else ' has')
    run.info_single(msg)
    return json.dumps(msg)

@route('/data/completeness', method='POST')
def completeness():
    completeness_stats = {}
    if not d.completeness:
        return json.dumps(completeness_stats)

    split_names = json.loads(request.forms.get('split_names'))
    bin_name = json.loads(request.forms.get('bin_name'))

    run.info_single('Completeness info has been requested for %d splits in %s' % (len(split_names), bin_name))

    completeness_stats = d.completeness.get_info_for_splits(set(split_names))

    return json.dumps({'stats': completeness_stats, 'refs': d.completeness.http_refs})


@route('/state/all')
def state_all():
    set_default_headers(response)

    return json.dumps(d.states_table.states)

@route('/state/get', method='POST')
def get_state():
    set_default_headers(response)

    name = request.forms.get('name')

    if name in d.states_table.states:
        state = d.states_table.states[name]
        return json.dumps(state['content'])

    return json.dumps("")

@route('/state/save', method='POST')
def save_state():
    if args.read_only:
        return json.dumps({'status_code': '0'})

    name = request.forms.get('name')
    content = request.forms.get('content')
    last_modified = datetime.datetime.now().strftime("%d.%m.%Y %H:%M:%S")

    d.states_table.store_state(name, content, last_modified)

    return json.dumps({'status_code': '1'})


#######################################################################################################################
# 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:
    server_process = Process(target=run_server, kwargs={'host': ip, 'port': port, 'quiet': True})
    server_process.start()

    if not args.server_only:
        webbrowser.open_new("http://%s:%d" % (ip, port))

    run.info_single('The server is now listening the port number "%d". When you are finished, press CTRL+C to terminate the server.' % 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(1)
