# IMPORT
# to work with files in folder 
import os

# to work with json files
import json
from urllib.request import urlopen

# to create the Flask app
from flask import Flask, render_template, request, url_for, redirect, jsonify, abort

# to import text contents and metadata from markdown files
from flaskext.markdown import Markdown
import frontmatter

# to cast string arguments into the required types
from pydoc import locate

# to work with notebooks
#
# to import notebook files
import nbimporter
nbimporter.options['only_defs'] = False
import importlib

# to read and execute the content of notebooks
import nbformat
from nbconvert import HTMLExporter, MarkdownExporter
from nbconvert.preprocessors import ExecutePreprocessor

# not sure about this is in the nbconvert documentation
from traitlets.config import Config

# to work with placeholder descriptions
from random import choice

# Subgroup Projects import

# not really happy of this routing for the module but
ac = importlib.import_module("projects.annotation-compass.annotation_compass")


# FUNCTIONS

def filenames(folder, remove_ext = False):
    ''' Read all the functions in a folder '''
    names = []
    for entry in os.scandir(folder):
        # add to the list only proper files
        if entry.is_file(follow_symlinks=False):
            # remove the extension from the filename
            n = os.path.splitext(entry.name)[0]
            if remove_ext:
                n = entry.name
            names.append(n)
    return names


def dirnames(folder):
    ''' Return all the folders in a folder '''
    names = []
    for entry in os.scandir(folder):
        # add to the list only proper files
        if not entry.name.startswith('.') and entry.is_dir():
            # remove the extension from the filename
            names.append(entry.name)
    return names


# not really sure about this file -> module -> function thing! 
# could someone help pls ? 
def get_function(name, folder):
    ''' Dynamic import a function from a folder '''
    file  = __import__(f'{folder}.{name}')
    module = getattr(file, name)
    function = getattr(module, name)
    return function


def get_function_info(function):
    ''' Extract info from a function '''
    name = function.__name__
    description = function.__doc__
    parameters = []
    output = ''

    # TODO: default values 
    
    # populate a list of tuple with patameter, type
    for param in function.__annotations__.keys():
        if param == 'return':
            output = function.__annotations__[param].__name__
        if param != 'return':
            parameters.append((param, function.__annotations__[param].__name__))
    
    return(name, description, parameters, output)

def print_info(function):
    ''' Print the info of a function nicely '''
    name, description, parameters, output = get_function_info(function)
    
    # very important feature
    from kaomoji.kaomoji import Kaomoji
    kao = Kaomoji()
    
    header = f'----------{kao.create()}'
    footer = '-' * len(header)
    
    print(header)
    print(name)
    print(description)
    print('Input:')
    for param, tp in parameters:
        print(f'  {param}, of type {tp}')
    print(f'Returns a {output}')
    print(footer)
    

def generate_function_list():
    ''' Build a list of the function ''' 
    functions = []
    for function in filenames("./notebooks"):
        try:
            fx = get_function(function, notebooks)
            name, description, parameters, output = get_function_info(fx)
#             print(f'{function} function is ok')
#             print(f'Description is: {description}')
            if description == None:
                description = placeholder_description('function')
            f = {
                "title": name,
                "description": description
            }
        except:
#             print('--->error so placeholder fx')
            f = {
                "title": function,
                "description": placeholder_description('function')
            }
        functions.append(f)
#     print(functions)
    with open('./static/functions.json', 'w') as file:
        json.dump(functions, file)
    
    
def placeholder_description(subject):
    ''' Return a placeholder description of a subject '''
    # ok this is a meme but is to make the notebooks a bit more waterproof
    adjs = ['useful', 'nice', 'intresting', 'incredible', 'wow']
    placeholders = [f'A super', f'A {choice(adjs)} and', f'You wont believe to this', f'Yet another' ]
    return f'{choice(placeholders)} {choice(adjs)} {subject}'
    
    
# optionally you can pass a boolean argument to execute the notebook before the export
# but it is a slow process so by default is not active
# TODO: markdown export instead of HTML ? 
# TODO: extract images from base64
def get_notebook_contents(filename, execute = False):
    ''' Export notebook contents as HTML. '''
    with open(filename) as f:
        nb = nbformat.read(f, as_version=4)
        
        if execute:
            ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
            ep.preprocess(nb, {'metadata':{'path':'notebooks/'}})
        
    html_exporter = HTMLExporter()    
    html_exporter.template_name = 'basic'    
    (body, resources) = html_exporter.from_notebook_node(nb)    
    
    return body

# EXPORT AS MARKDOWN
def get_notebook_md(filename, execute = False):
    ''' Export notebook contents as Markdown. '''
    with open(filename) as f:
        nb = nbformat.read(f, as_version=4)
        
        if execute:
            ep = ExecutePreprocessor(timeout=600, kernel_name='python3')
            ep.preprocess(nb, {'metadata':{'path':'notebooks/'}})
        
    md_exporter = MarkdownExporter()    
    
    (body, resources) = md_exporter.from_notebook_node(nb)    
    
    return body

def get_contents(filename, directory = './contents'):
    ''' Return contents from a filename as frontmatter handler '''
    with open(f"{directory}/{filename}", "r") as f:
        content = frontmatter.load(f)
    return content

def get_meta(filename, directory = './contents'):
    ''' Return contents from a filename as frontmatter handler '''
    with open(f"{directory}/{filename}", "r") as f:
         metadata, content = frontmatter.parse(f.read())
    return metadata

#  EVENT MODE

def is_event_mode():
    return False



# FLASK APP

base_url = "si16"
notebooks = "notebooks"
projects = "projects"


# create flask application
app = Flask(__name__,             
            static_url_path=f'/soupboat/{base_url}/static', 
            static_folder=f'/soupboat/{base_url}/static')
Markdown(app, extensions=['extra'])



# add the base_url variable to all the flask templates
@app.context_processor
def set_base_url():
    return dict(base_url = base_url)

# Generate functions list
generate_function_list()

# For specific pages we can build and link dedicated templates


# Homepage
@app.route(f"/{base_url}/")
def home_page():
    
    # get the basic info of the website from the /contents/si16_info.md file
    info = get_contents("si16_info.md").to_dict()
    
    
    projects_list = []
    for project in dirnames("./projects"):
        page = get_contents("documentation.md", f"./{projects}/{project}").to_dict()
        page['slug'] = project
        projects_list.append(page)
    
    
    
    # get the list of the projects, the functions, and the corpora    
    home = {
        **info,
        "projects": projects_list,
        "functions": filenames("./notebooks"),
        "corpora": dirnames("./static/corpora")
    }
    return render_template("home.html", **home)


# # About Page
# @app.route(f"/{base_url}/about/")
# def about_page():
#     about = get_contents('about.md')
#     return render_template("about.html", title = about['title'], description = about['description'], contents=about.content)

# For generic pages we can include a common template and change only the contents
@app.route(f"/{base_url}/<slug>/")
def dynamic_page(slug = None):
    try:
        page = get_contents(f"{slug}.md").to_dict()
        # page is a dictionary that contains: 
        # - all the attributes in the markdown file (ex: title, description, soup, etc)
        # - the content of the md in the property 'content'
        # in this way we can access those frontmatter attributes in jinja simply using the variables title, description, soup, etc
        return render_template("page.html", **page)
    except FileNotFoundError:
        # TODO: a proper not found page
        return render_template('404.html')



# List of projects
@app.route(f"/{base_url}/projects/")
def projects_list():
    # get a list of the functions from the notebooks folder
    projects_list = []
    for project in dirnames("./projects"):
        page = get_contents("documentation.md", f"./{projects}/{project}").to_dict()
        page['slug'] = project
        projects_list.append(page)

    info = get_contents("projects.md").to_dict()
       
    # generate a link to each function
    return render_template("projects.html", projects=projects_list, **info)


# Single project
@app.route(f"/{base_url}/projects/<project>/")
def p_info(project = None):
    try:
        
        page = get_contents("documentation.md", f"./{projects}/{project}").to_dict()
        
        if 'showcases' in dirnames(f"./{projects}/{project}"):
            page['showcases'] = []
            showcases_list = filenames(f"./{projects}/{project}/showcases/")
            for showcase in showcases_list:
                print(showcase)
                info = get_meta(showcase + '.md', f"./{projects}/{project}/showcases/")
                info['slug'] = showcase
                page['showcases'].append(info)

        return render_template("project.html", **page)
    except FileNotFoundError:
        return render_template('404.html')
    

@app.route(f"/{base_url}/projects/<project>/<showcase>/")
def p_showcase(project = None, showcase = None):
    print(project)
    print(showcase)
    try:
        page = get_contents(f"{showcase}.md", f"./{projects}/{project}/showcases").to_dict()
        return render_template('page.html', **page)
    except FileNotFoundError:
         return render_template('404.html')
    return render_template('404.html')
    
# List of functions
@app.route(f"/{base_url}/functions/")
def functions_list():
    
    
    info = get_contents("functions.md").to_dict()
    
    # get a list of the functions from the notebooks folder
    functions = filenames("./notebooks")
    
    with open('./static/functions.json', 'r') as file:
        functions = json.load(file)

        
    # generate a link to each function
    return render_template("functions.html", functions=functions, **info)


# Single Function page
@app.route(f"/{base_url}/functions/<function>/")
def f_info(function=None):
    if function in filenames(f"./{notebooks}"):
        fx = get_function(function, notebooks)
        name, description, parameters, output = get_function_info(fx)

        # executing a notebook takes a lot of time mmm should we just send them without the results of the examples? or save it executed?
#         documentation = get_notebook_contents(f"./{notebooks}/{function}.ipynb")

        documentation = get_notebook_md(f"./{notebooks}/{function}.ipynb")
        
        return render_template("function.html", title=name, description=description, parameters=parameters, output=output, documentation=documentation)

    # TODO: meaningful error code return
    else:
        return render_template('404.html')

    
    
# Function API page

@app.route(f"/{base_url}/api/<function>/")
def f_api(function=None):
    if function in filenames(f"./{notebooks}"):

        fx = get_function(function, notebooks)
        name, description, parameters, output = get_function_info(fx)

        query_params = []
        for param, tp in parameters:
            a = request.args.get(param)
            # cast the type of the argument to the type that the function requires
            if tp == "list":
                a = request.args.getlist(param, type=str)
            else:
                tp = locate(tp)
                a = tp(a)
            # print(a)
            query_params.append(a)
        return fx(*query_params)

    # TODO: meaningful error code return
    return "mmmm there is no function with this name sorry"



# TODO: maybe the event mode could be a decorator ? to avoid the request from the client every time and instead just do it in the server ? 
# EVENT MODE 
import mimetypes

@app.route(f"/{base_url}/api/is-event-mode/")
def event_mode():
    if is_event_mode():
        event_path = '/static/event'
        event = dirnames("./static/event")    
        page = request.values.get('page', '')
        snippets = []
        if page in event:
            snippets = []
            for entry in os.scandir(f".{event_path}/{page}"):
                # add to the list only proper files
                if entry.is_file(follow_symlinks=False):
                    snippet = {
                    "name": entry.name
                    }

                    ty, enc = mimetypes.guess_type(entry.name)

                    if 'text' in ty:
                        snippet['type'] = 'text'
                        with open(f".{event_path}/{page}/{entry.name}", 'r') as f:
                            content = f.read()
                            snippet['content'] = content
                    elif 'image' in ty:
                        snippet['type'] = 'image'
                        snippet['content'] = f"{event_path}/{page}/{entry.name}"
                    elif 'video' in ty:
                        snippet['type'] = 'video'
                        snippet['content'] = f"{event_path}/{page}/{entry.name}"  
                    elif 'audio' in ty:
                        snippet['type'] = 'audio'
                        snippet['content'] = f"{event_path}/{page}/{entry.name}"

                    snippets.append(snippet)
            return {"event": True, "snippets": snippets}
        return {"event": True}
    return {"event": False}






# PROJECTS API pages
# custom section ===========
# warning mothership reporting
# we will add here the custom endpoints for each subgroup project



# 2MB max for file upload (we can change this)
app.config['MAX_CONTENT_LENGTH'] = 2 * 1000 * 1000 

@app.route(f"/{base_url}/annotation-compass/", methods=['GET','POST'])
def annotation_compass():
    if request.method == 'POST':
        # Upload new file
        ac.upload_file(request)
    images = ac.list_images()
    return render_template('annotation-compass.html', title='Annotation Compass', images=images)

@app.route(f"/{base_url}/annotation-compass/annotate/<image>/")
def annotate_image(image=None):
    print(app.static_url_path, app.static_folder)
    description = ''
    try:
        description = ac.get_image_description(image)
    except:
        print('there is no description')
    return render_template('annotate_image.html', image=image, description=description)

@app.route(f"/{base_url}/annotation-compass/add-label/", methods=['GET','POST'])
def insert_label():
    if request.method == 'POST':
        return ac.insert_label(request)

@app.route(f"/{base_url}/annotation-compass/get-labels/<image>/")
def get_labels_list(image=None):
    return ac.get_labels_list(image)











# JIAN API

def generate_urls(elements):
    a_list=''
    for element in elements:
        url=f'https://hub.xpub.nl/soupboat/individual-maps/{element}/'
        a = f'<a href="{url}">{element}</a><br />'
        a_list = a_list + a
    return a_list

@app.route(f"/{base_url}/api/label/rejection/individual_map/")
def individual_map():
    
    image = 'xpub1_rotterdam_map_1400px.jpg'
    url = f"https://hub.xpub.nl/soupboat/generic-labels/get-labels/?image={image}"
    response = urlopen(url)
    data_json = json.loads(response.read())

    all_userIDs = []
    for label in data_json['labels']:
        all_userIDs.append(label['userID'])
    set_userIDs = set(all_userIDs)

    im = get_function('individual_map', notebooks)
    
    individual_labels = im(data_json['labels'], ['5058763759','5941298752'])
    
    to_html = get_function('html_tag', notebooks)
    result = to_html(individual_labels, True, True, True, True)
    
    return result
    

@app.route(f"/{base_url}/projects/map/rejection_map/")
def rejection_map():
    return render_template('rejection_map.html')
    


# ===============
# Error handlers!
@app.errorhandler(400)
def error_400(e):
    # bad request or invalid url
    return render_template('400.html'), 400

@app.errorhandler(403)
def error_403(e):
    # forbidden (for invalid key) for evemt mode when it's not the 17th of the month
    return render_template('403.html'), 403

@app.errorhandler(404)
def error_404(e):
    # page not found
    return render_template('404.html'), 404

@app.errorhandler(500)
def error_500(e):
    # internal server error
    return render_template('500.html'), 500

@app.errorhandler(502)
def error_502(e):
    # bad gateaway
    return render_template('502.html'), 502

@app.errorhandler(503)
def error_503(e):
    # service temporarily unavailable, for secret breaks
    return render_template('503.html'), 503

@app.errorhandler(504)
def error_503(e):
    # ???????? gateway timeout shall we put it?????
    return render_template('504.html'), 504



# RUN

app.run(port="3131")
