import commonmark
import json
import os
import dokk.sparql as sparql
import urllib.parse

from bottle import abort, get, post, redirect, request, response, route, static_file
from datetime import datetime, timezone
from dokk import article_template, graph, graph_public, settings, template, url, user
from string import Template

class requires_signin(object):
    """
    This is a decorator used for controller to make sure a user is signed in
    before entering the controller.
    """
    
    def __init__(self, controller):
        self.controller = controller

    def __call__(self, *args, **kargs):
        the_user = user.get_from_session()
        
        if not the_user:
            return redirect(url('signin') + '?next=' + urllib.parse.quote(request.url, safe=''))
        else:
            return self.controller(*args, **kargs)

class requires_signout(object):
    """
    This is a decorator used for controller to make sure a user is signed out
    before entering the controller.
    """
    
    def __init__(self, controller):
        self.controller = controller

    def __call__(self, *args, **kargs):
        the_user = user.get_from_session()
        
        if not the_user:
            return self.controller(*args, **kargs)
        else:
            return redirect(url('user', username=the_user['username']))

#### HACK: This is used to retrieve documentation, but it should not stay here!
####       The only purpose of these routes is to maintain compatibility with
####       the old website until I find a better solution.
###############################################################################
@get('/documentation', name='documentation')
def docs():
    """List of documentations and versions."""
    
    # Some page attributes
    page = { 'title': 'Documentation' }
    
    # Get list of available docs
    docs = sparql.query_public("""
        PREFIX :       <dokk:/>
        PREFIX dokk:   <https://ontology.dokk.org/>
        PREFIX schema: <http://schema.org/>
        
        DESCRIBE *
        WHERE
        {
            ?docs a dokk:Documentation .
        }
    """)
    
    docs = docs['@graph']
    
    return template('documentation/index.html', dokk=page, docs=docs)

@get('/documentation/<id>', name='documentationproject')
def documentationproject(id):
    """List of documentations and versions."""
    
    # Some page attributes
    page = { 'title': '' }
    
    doc = sparql.query_public(Template ("""
        PREFIX :       <dokk:>
        PREFIX dokk:   <https://ontology.dokk.org/>
        PREFIX schema: <http://schema.org/>
        
        DESCRIBE *
        WHERE
        {
            ?doc a dokk:Documentation ;
                  dokk:id "$id" .
        }
    """).substitute({
        'id': id
    }))
    
    page['title'] = doc['name'] + ' documentation'
    # doc = doc['@graph']
    
    return template('documentation/project.html', dokk=page, doc=doc)

@get('/documentation/<docname>/<release>/', name='documentationdocs')
@get('/documentation/<docname>/<release>/<path:path>/')
def docs_index(docname, release, path=''):
    location = settings['dokk.archive'] + '/documentation/html/' + docname + '/' + release + '/'
    
    return static_file(path + '/index.html', root=location)

@get('/documentation/static/<file:path>', name='documentationstatic')
def docsstatic(file):
    return bottle.static_file(file, root=self.root+'static')
        
@get('/documentation/<docname>/<release>/<filename:path>')
def docs_static(docname, release, filename):
    location = settings['dokk.archive'] + '/documentation/html/' + docname + '/' + release + '/'
    
    return static_file(filename, root=location)
###############################################################################

@route('/<filename:re:(favicon.ico|robots.txt)>')
def public_assets(filename):
    """
    Tell Bottle where it can find some special files that are not inside /static
    This is needed because favicon.ico, robots.txt, and other files are expected
    to be found under the root path, and they should not be misunderstood as
    node IDs.
    
    TODO: find a better way to serve these static files.
    
    :param filename: the name of the file
    :return: 
    """
    
    return static_file(filename, root='./')

@get(settings['dokk.reserved_path'] + '/static/<filename:path>', name='static_assets')
def static_assets(filename):
    """
    Path containing the static files.
    
    :param filename: The name of the static asset.
    :return: 
    """
    
    return static_file(filename, root='./dokk/static')

@get(settings['dokk.reserved_path'] + '/explore', name='explore')
def explore():
    """
    Explore the DOKK graph.
    """
    
    return template('explore.html')

@get(settings['dokk.reserved_path'] + '/explorenodes')
def explore_nodes():
    """
    The "explore" controller loads the page template, from where a AJAX request
    is sent to this controller to retrieve the list of nodes to be displayed.
    """
    
    return graph.get_topics()

@get(settings['dokk.reserved_path'] + '/search', name='search')
def search():
    """
    Search the DOKK.
    """
    
    if 'q' not in request.GET:
        return redirect(url('index'))
    
    search_term = request.GET.getunicode('q').strip()
    
    if len(search_term) == 0:
        return redirect(url('index'))
    
    results = graph.search_articles(search_term, limit=100)['results']['bindings']
    
    return template('search.html', search_term=search_term, results=results)

@get(settings['dokk.reserved_path'] + '/suggest', name='suggest')
def suggest():
    """
    Search suggestions when typing in the DOKK search bar.
    """
    
    if 'q' not in request.GET:
        return ''
    
    search_term = request.GET.getunicode('q').strip()
    
    if len(search_term) == 0:
        return ''
    
    articles = graph_public.list_topics(search_term)['results']['bindings']
    
    return template('suggest.html', search_term=search_term, articles=articles)

@get(settings['dokk.reserved_path'] + '/editor', name='editor')
@requires_signin
def dokk_editor():
    return template('editor/index.html')

@post(settings['dokk.reserved_path'] + '/editor')
@requires_signin
def dokk_editor_new_node():
    """
    Handle the <form> POSTs from the Editor's homepage to create new
    topics/nodes/templates/queries. This controller basically just takes the POST
    data and redirects the user to the edit page.
    """
    
    if 'type' not in request.forms or \
       'id'   not in request.forms:
        return redirect(url('editor'))
    
    type = request.forms.getunicode('type')
    id   = request.forms.getunicode('id')
    
    if type not in [ 'topic', 'template', 'query' ]:
        return redirect(url('editor'))
    
    return redirect(url('edit', type=type, id=id))

@get(settings['dokk.reserved_path'] + '/editor/<type:page>/<id:path>', name='edit')
@requires_signin
def edit(type, id):
    """
    Edit a page.
    
    :param type: the type of information to edit (article, template, ...)
    :param id: ID of the node to edit.
    """
    
    # Normalize page ID
    normalized_id = graph.normalize_name(id)
    if normalized_id != id:
        return redirect(url('edit', type=type, id=normalized_id))
    
    # Read existing page
    page = type + '/' + id
    if graph.exists(page):
        revision = graph.get_last_revision(page)
        content = revision['content']
        revision_number = revision['dokk:revision_number']
    else:
        revision_number = None
        
        if type == 'topic':
            content = template('node/topic.ttl', node_id=id)
        elif type == 'article':
            content = template('node/article.md')
        elif type == 'query':
            content = template('node/query.sparql')
        elif type == 'template':
            content = template('node/template.html')
        else:
            content = ''
    
    return template('editor/edit.html',
                    view='edit',
                    type=type, node_id=id, content=content,
                    revision_number=revision_number)

@post(settings['dokk.reserved_path'] + '/editor/<type:page>/<id:path>')
@requires_signin
def save_edits(type, id):
    """
    Save user edits for a page.
    """
    
    # Retrieve user
    author = user.get_from_session()
    
    if not author:
        return redirect(url('index'))
    
    # Retrieve <form> data
    rev_number = request.forms.getunicode('revision').strip()
    content = request.forms.getunicode('content').strip()[:10485760]
    edit_summary = request.forms.getunicode('edit_summary').strip()[:1024]
    
    # Ignore completely if there is no edit_summary. This is just a bad request.
    if not edit_summary:
        return redirect(url('node', id=id))
    
    # The ID for this page
    page = type + '/' + id
    
    # Save a new revision for this page
    added = graph.add_revision(page, rev_number, author['username'], content, edit_summary)
    
    # If the revision hasn't been added, there was a conflict. Show merge page.
    if not added:
        current_revision = graph.get_last_revision(page)
        
        return template('editor/merge.html',
                        view='merge',
                        type=type, node_id=id, content=content,
                        current_revision=current_revision,
                        revision_number=current_revision['dokk:revision_number'],
                        edit_summary=edit_summary)
    
    # Revision was added successfully!
    
    current_revision = graph.get_last_revision(page)
    
    if type == 'article':
        graph.add_article(id)
        return redirect(url('node', id=id))
    
    if type == 'file':
        graph.add_file(id)
        return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
    
    if type == 'query':
        graph.add_query(id)
        return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
    
    if type == 'template':
        graph.add_template(id)
        
        return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
    
    if type == 'topic':
        graph.add_topic(id)
        return redirect(url('node', id=id))

@get(settings['dokk.reserved_path'] + '/revisions/<type>/<id:path>', name='revisions')
def show_revisions(type, id):
    """
    Show a list of revisions for one page.
    
    :param type: the type of information to edit (article, template, ...)
    :param id: ID of the node to edit.
    """
    
    page_name = type + '/' + id
    revisions = graph.list_revisions(page_name)['results']['bindings']
    
    return template('editor/revisions.html',
                    view='revision',
                    type=type, node_id=id, page_name=page_name,
                    revisions=revisions)

@get(settings['dokk.reserved_path'] + '/revision/<number:int>/<type>/<id:path>', name='revision')
def show_revision(number, type, id):
    """
    Show an old revision.
    
    :param number: The revision number.
    :param type: the type of information to edit (article, template, ...)
    :param id: ID of the node to edit.
    """
    
    page_name = type + '/' + id
    revision = graph.get_revision(page_name, number)
    current_revision = graph.get_last_revision(page_name)
    
    return template('editor/old_revision.html',
                    view='revision',
                    type=type, node_id=id, page_name=page_name,
                    revision=revision, current_revision=current_revision)

@get(settings['dokk.reserved_path'] + '/upload', name='upload')
@requires_signin
def dokk_upload():
    return template('editor/upload.html')

@post(settings['dokk.reserved_path'] + '/upload')
@requires_signin
def dokk_upload_handle():
    """
    Handle uploaded file.
    """
    
    # Store errors here (if file cannot be saved for some reasons)
    errors = []
    
    # The file being uploaded
    blob = request.files.get('blob')
    
    # Retrieve form data
    # Note about Bottle: request.forms['key'] or request.forms.get("key") return
    # the raw parameter, the byte string. Instead, request.forms.key and
    # request.forms.getunicode("key") return the UTF-8 string. Normally we use
    # request.forms.getunicode("key") for consistency and also to avoid any doubts,
    # but when using <form enctype="multipart/form-data"> the request body is
    # already a UTF-8 string and request.forms.getunicode() tries to encode a
    # string that is already UTF-8 encoded. So the value that is returned is either
    # an empty string or None, therefore we use the "raw" value from the request.
    # It's probably a Bottle bug that needs fixing upstream.
    file = {
        'name':        request.forms['name'],
        'license':     request.forms['license'],
        'source':      request.forms['source'],
        'description': request.forms['description']
    }
    
    # Clear input data
    file['name']        = file['name'].strip()
    file['name']        = graph.normalize_name(file['name'])
    file['name']        = file['name'].replace('/', '-')
    file['license']     = file['license'].strip()
    file['source']      = file['source'].strip()
    file['description'] = file['description'].strip()
    
    # Where to save the file
    file['path'] = settings['dokk.archive'] + '/' + file['name']
    
    # File name must have an extension
    if '.' not in blob.filename:
        errors.append('no_extension')
    
    # Validate file name
    if len(file['name']) == 0:
        errors.append('name')
    
    # Validate file extension
    if os.path.splitext(blob.filename)[1] != os.path.splitext(file['name'])[1]:
        errors.append('extension_mismatch')
    
    # Validate license
    if len(file['license']) == 0:
        errors.append('license')
    
    # Make sure a file with this name does not exist
    if os.path.exists(file['path']):
        errors.append('path')
    
    if len(errors) > 0:
        return template('editor/upload.html', errors=errors, file=file)
    
    # Everything fine! We can save the file
    
    # Retrieve user
    author = user.get_from_session()
    
    if not author:
        return redirect(url('editor'))
    
    # Store blob to filesystem
    blob.save(file['path'])
    
    # Automatically create a new revision of this file's properties
    graph.add_revision(
        'file/' + file['name'],
        None,
        author['username'],
        template('node/file.ttl', filename=file['name'],
                                  license=file['license'],
                                  primary_source=file['source'],
                                  description=file['description']),
        'New file upload.')
    
    # Automatically create a new node with type "dokk:File"
    graph.add_file(file['name'])
    
    # Redirect to the file edit page
    return redirect(url('edit', type='file', id=file['name']))

@get(settings['dokk.reserved_path'] + '/templates', name='templates')
@requires_signin
def templates():
    """
    Show a list of all templates available on this DOKK instance.
    """
    
    templates = graph.list_templates()['results']['bindings']
    
    return template('editor/templates.html', templates=templates)

@get(settings['dokk.reserved_path'] + '/queries', name='queries')
@requires_signin
def queries():
    """
    Show a list of all queries available on this DOKK instance.
    """
    
    queries = graph.list_queries()['results']['bindings']
    
    return template('editor/queries.html', queries=queries)

@get(settings['dokk.reserved_path'] + '/files', name='files')
@requires_signin
def files():
    """
    Show a list of all uploaded files on this DOKK instance.
    """
    
    all_files = [ f for f in os.listdir(settings['dokk.archive'])
                          if os.path.isfile(os.path.join(settings['dokk.archive'], f))]
    
    return template('editor/files.html', files=all_files)

@get(settings['dokk.reserved_path'] + '/file/<filename>', name='file')
def show_file(filename):
    """
    Show a file and its properties. To return the raw binary, see "show_blob()"
    """
    
    """
    path = settings['dokk.archive'] + '/' + filename
    
    if not os.path.isfile(path):
        return abort(404)
    """
    
    node = graph.get_file(filename)
    
    return template('editor/file.html', filename=filename, node=node)

@get(settings['dokk.reserved_path'] + '/blob/<filename>', name='blob')
def show_blob(filename):
    """
    Return a raw file from the repository.
    """
    
    return static_file(filename, root=settings['dokk.archive'])

@get(settings['dokk.reserved_path'] + '/signin', name='signin')
@requires_signout
def signin():
    """
    The sign in page.
    """

    return template('signin.html')

@post(settings['dokk.reserved_path'] + '/signin')
@requires_signout
def signin_check():
    """
    Check sign in form.
    """
    
    username = request.forms.getunicode('username')
    password = request.forms.getunicode('password')
    remember = 'remember' in request.forms
    
    if not username or not password:
        return template('login.html',
                        flash = 'Bad login!')
    
    # Retrieve user from database
    the_user = user.get_from_password(username, password)
    
    # Username/Password not working
    if 'username' not in the_user:
        return template('signin.html',
                        flash = 'Bad login!')
    
    # Stop banned users
    if user.is_banned(username):
        return template('signin.html',
                        flash = 'Bad login!')
    
    # ... Everything OK?
    
    # Start new browser session
    user.start_session(username, remember)
    
    # Redirect to URL if "?next=" is set in the URL
    if 'next' in request.GET:
        return redirect(request.GET.getunicode('next'))
    
    # otherwise redirect to homepage
    return redirect(url('editor'))

@get(settings['dokk.reserved_path'] + '/register', name='register')
@requires_signout
def register():
    """
    Register new account.
    """
    
    return template('register.html')

@post(settings['dokk.reserved_path'] + '/register')
@requires_signout
def register_new_account():
    """
    Check form for creating new account.
    """
    
    username = request.forms.getunicode('username')
    password = request.forms.getunicode('password')
    
    # Normalize username
    username = username.strip()
    
    if len(username) == 0:
        return None
    
    # Check if username already exists.
    if user.exists(username):
        return template('register.html',
                        flash='Name already taken, please choose another one.')
    
    # Password too short?
    if len(password) < 8:
        return template ('register.html',
                         flash = 'Password too short.')
    
    # Username OK, Password OK: create new user
    user.create(username, password)
    
    # Retrieve user (to check if it was created)
    new_user = user.get_from_password(username, password)
    
    # Something bad happened...
    if 'username' not in new_user:
        return template('register.html',
                        flash = 'An error has occurred, please try again.')
    
    # Start session...
    user.start_session(username)
    
    # ... and go to the homepage
    return redirect(url('index'))

@get(settings['dokk.reserved_path'] + '/signout', name='signout')
@requires_signin
def signout():
    """
    Logout user and return to homepage.
    """
    
    user.end_session()
    
    redirect(url('index'))

@get(settings['dokk.reserved_path'] + '/user/<username>', name='user')
def show_user(username):
    """
    Show a user homepage.
    """
    
    return template('user/homepage.html')

@route(settings['dokk.reserved_path'])
@route(settings['dokk.reserved_path'] + '/<path:path>')
def reserved_paths(path=None):
    """
    Forbid users from editing any page starting with the reserved path.
    """
    
    return abort(403) # 403 - Forbidden

@get('/<id:path>', name='node')
def graph_node(id):
    """
    Read a node's article. This controller must be kept at the end,
    because it catches all routes that have not been caught by other routes.
    """
    
    # Normalize page ID
    normalized_id = graph.normalize_name(id)
    
    # Redirect to the correct page
    if normalized_id != id:
        return redirect(url('node', id=normalized_id))
    
    # This topic (node) doesn't exist in the database
    if not graph_public.topic_exists(normalized_id):
        response.status = 404 # Not found
        return template('article_not_found.html', node_id=id)
    
    # Retrieve topic node
    node = graph_public.get_topic(id)
    
    # Sort templates by name
    node_types = sparql.expand(node)[0]['@type']
    node_types.sort()
    
    # Move dokk:Topic in first position
    if 'https://ontology.dokk.org/Topic' in node_types:
        node_types.remove('https://ontology.dokk.org/Topic')
        node_types.insert(0, 'https://ontology.dokk.org/Topic')
    
    # Concatenate all templates into a single page
    page = ''
    for type_id in node_types:
        page += "{% include '" + type_id + "' %}\n"
    
    # Render templates for node types
    page = article_template(page + '\n', node=node)
    
    # Page variables
    dokk = { 'title': node['title'] if 'title' in node else node['id'].replace('_', ' ') }
    
    return template('article.html', dokk=dokk, node=node, page=page)
    
@get('/', name='index')
def index():
    """
    Homepage.
    """
    
    return template('homepage.html')

