# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                             #
#   OpenBench is a chess engine testing framework authored by Andrew Grant.   #
#   <https://github.com/AndyGrant/OpenBench>           <andrew@grantnet.us>   #
#                                                                             #
#   OpenBench is free software: you can redistribute it and/or modify         #
#   it under the terms of the GNU General Public License as published by      #
#   the Free Software Foundation, either version 3 of the License, or         #
#   (at your option) any later version.                                       #
#                                                                             #
#   OpenBench is distributed in the hope that it will be useful,              #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of            #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             #
#   GNU General Public License for more details.                              #
#                                                                             #
#   You should have received a copy of the GNU General Public License         #
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.     #
#                                                                             #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import django.http
import django.shortcuts
import django.contrib.auth

import OpenBench.config
import OpenBench.utils

from OpenBench.models import *
from django.contrib.auth.models import User

from django.db.models import F
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from htmlmin.decorators import not_minified_response

def render(request, template, data={}):

    template = 'OpenBench/{0}'.format(template)

    data.update({'config' : OpenBench.config.OPENBENCH_CONFIG})

    if request.user.is_authenticated:

        profile = Profile.objects.filter(user=request.user)
        data.update({'profile' : profile.first()})

        if profile.first() and not profile.first().enabled:
            data.update({'error' : data['config']['error']['disabled']})

        if request.user.is_authenticated and not profile.first():
            data.update({'error' : data['config']['error']['fakeuser']})

    return django.shortcuts.render(request, template, data)


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                            ADMINISTRATIVE VIEWS                             #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

def register(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return the HTML template used for registering a new User        #
    #                                                                         #
    #  POST : Enforce matching alpha-numeric passwords, and then attempt to   #
    #         generate a new User and Profile. Return to the homepage after   #
    #         after logging the User in. Share any errors with the viewer     #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if request.method == 'GET':
        return render(request, 'register.html')

    if request.POST['password1'] != request.POST['password2']:
        return index(request, error='Passwords Do Not Match')

    if not request.POST['username'].isalnum():
        return index(request, error='Alpha Numeric Usernames Only')

    if User.objects.filter(username=request.POST['username']):
        return index(request, error='That Username is taken')

    email    = request.POST['email']
    username = request.POST['username']
    password = request.POST['password1']

    user = User.objects.create_user(username, email, password)
    django.contrib.auth.login(request, user)
    Profile.objects.create(user=user)

    return django.http.HttpResponseRedirect('/index/')

def login(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return the HTML template used for logging in a User             #
    #                                                                         #
    #  POST : Attempt to login the User and authenticate their credentials.   #
    #         If their login is invalid, let them know. In all cases, return  #
    #         the User back to the main page                                  #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if request.method == 'GET':
        return render(request, 'login.html')

    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])

    if user is None:
        return index(request, error='Unable to Authenticate User')

    django.contrib.auth.login(request, user)
    return django.http.HttpResponseRedirect('/index/')

def logout(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Logout the User if they are logged in. Return to the main page  #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    django.contrib.auth.logout(request)
    return django.http.HttpResponseRedirect('/index/')

def profile(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : If the User is logged in, return the HTML template which shows  #
    #         all of the information about the User, and a form to change the #
    #         email address, password, and default engine of the User. If the #
    #         User is not logged in, return them to the main page             #
    #                                                                         #
    #  POST : Modify the User's email address and selected Engine, if the     #
    #         User has requested this. Update the password for the User if    #
    #         they have requested a change and provided a new set of matching #
    #         passwords. Return the User to the main page                     #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if not request.user.is_authenticated:
        return django.http.HttpResponseRedirect('/login/')

    if request.method == 'GET':
        return render(request, 'profile.html')

    profile = Profile.objects.filter(user=request.user)
    profile.update(engine=request.POST['engine'], repo=request.POST['repo'])

    request.user.email = request.POST['email']
    request.user.save()

    if request.POST['password1'] != request.POST['password2']:
        return index(request, error='Passwords Do Not Match')

    if request.POST['password1'] != '':
        request.user.set_password(request.POST['password1'])
        django.contrib.auth.login(request, request.user)
        request.user.save()

    return django.http.HttpResponseRedirect('/index/')


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                               TEST LIST VIEWS                               #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

def index(request, page=1, error=''):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return all pending, active, and completed tests. Limit the      #
    #         display of tests by the requested page number. Also display the #
    #         status for connected machines.                                  #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    pending   = OpenBench.utils.getPendingTests()
    active    = OpenBench.utils.getActiveTests()
    completed = OpenBench.utils.getCompletedTests()

    start, end, paging = OpenBench.utils.getPaging(completed, page, 'index')

    data = {
        'error'  : error,  'pending'   : pending,
        'active' : active, 'completed' : completed[start:end],
        'paging' : paging, 'status'    : OpenBench.utils.getMachineStatus(),
    }

    return render(request, 'index.html', data)

def greens(request, page=1):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return all tests both passed and completed. Limit the display   #
    #         of tests by the requested page number.                          #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    completed = OpenBench.utils.getCompletedTests().filter(passed=True)
    start, end, paging = OpenBench.utils.getPaging(completed, page, 'greens')

    data = {'completed' : completed[start:end], 'paging' : paging}
    return render(request, 'index.html', data)

def search(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return the HTML template for searching tests on the framework.  #
    #                                                                         #
    #  POST : Filter the tests by the provided criteria, and return a display #
    #         with the filtered results. Keywords are case insensitive        #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if request.method == 'GET':
        return render(request, 'search.html', {})

    tests = Test.objects.all()
    keywords = request.POST['keywords'].upper().split()
    if not keywords: keywords = [""]

    if request.POST['engine'] != '':
        tests = tests.filter(engine=request.POST['engine'])

    if request.POST['author'] != '':
        tests = tests.filter(author=request.POST['author'])

    if request.POST['showgreens'] == 'False':
        tests = tests.exclude(passed=True)

    if request.POST['showyellows'] == 'False':
        tests = tests.exclude(failed=True, wins__gte=F('losses'))

    if request.POST['showreds'] == 'False':
        tests = tests.exclude(failed=True, wins__lt=F('losses'))

    if request.POST['showunfinished'] == 'False':
        tests = tests.exclude(passed=False, failed=False)

    if request.POST['showdeleted'] == 'False':
        tests = tests.exclude(deleted=True)

    filtered = [
        test for test in tests.order_by('-updated') if
        any(keyword in test.dev.name.upper() for keyword in keywords)
    ]

    return render(request, 'search.html', {'tests' : filtered})

def user(request, username, page=1):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return all pending, active, and completed tests for the User    #
    #         that has been requested. Limit the display of completed tests   #
    #         by the requested page number. Also display the User's machines  #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    pending   = OpenBench.utils.getPendingTests().filter(author=username)
    active    = OpenBench.utils.getActiveTests().filter(author=username)
    completed = OpenBench.utils.getCompletedTests().filter(author=username)

    url = 'user/{0}'.format(username)
    start, end, paging = OpenBench.utils.getPaging(completed, page, url)

    data = {
        'pending'   : pending,              'active' : active,
        'completed' : completed[start:end], 'paging' : paging,
        'status'    : OpenBench.utils.getMachineStatus(username),
    }

    return render(request, 'index.html', data)


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                           GENERAL DATA TABLE VIEWS                          #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

def users(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return information about all users on the Framework. Sort the   #
    #         Users by games completed, tests created. The HTML template will #
    #         filter out disabled users later.                                #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    data = {'profiles' : Profile.objects.order_by('-games', '-tests')}
    return render(request, 'users.html', data)

def events(request, page=1):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return information about the events taken on the Framework.     #
    #         Only show those events for the requested page.                  #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    events = LogEvent.objects.all().order_by('-id')
    start, end, paging = OpenBench.utils.getPaging(events, page, 'events')

    data = {'events' : events[start:end], 'paging' : paging};
    return render(request, 'events.html', data)

def machines(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return information about all of the machines that have been     #
    #         active on the Framework within the last fifteen minutes         #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    data = {'machines' : OpenBench.utils.getRecentMachines()}
    return render(request, 'machines.html', data)


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                            TEST MANAGEMENT VIEWS                            #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

def test(request, id, action=None):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : The User is either trying to view the status of the selected    #
    #         test, or adjust the running state of the test in some way. When #
    #         viewing a test, collect the results and return an HTML template #
    #         using that data. Otherwise, look to adjust the running state of #
    #         the test. We throw out any invalid requests. Create a LogEvent  #
    #         if the we attempt to modify the test's state in any way         #
    #                                                                         #
    #  POST : The only valid POST request is for the action MODIFY. Requests  #
    #         to modify contain an updated Priority and Throughput parameter  #
    #         for the selected test. Bound the updated values, and log the    #
    #         modification of the test with the creation of a new LogEvent    #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if not Test.objects.filter(id=id):
        return django.http.HttpResponseRedirect('/index/')

    if action not in ['APPROVE', 'RESTART', 'STOP', 'DELETE', 'MODIFY']:
        test = Test.objects.get(id=id)
        results = Result.objects.filter(test=test).order_by('machine_id')
        data = {'test' : test, 'results': results}
        return render(request, 'test.html', data)

    if not request.user.is_authenticated:
        return django.http.HttpResponseRedirect('/login/')

    user = request.user
    test = Test.objects.get(id=id)
    profile = Profile.objects.get(user=user)

    if not profile.approver and test.author != profile.user.username:
        return django.http.HttpResponseRedirect('/index/')

    if action == 'APPROVE':
        if test.author == user.username and not user.is_superuser:
            return django.http.HttpResponseRedirect('/index/')

    if action == 'APPROVE': test.approved =  True; test.save()
    if action == 'RESTART': test.finished = False; test.save()
    if action == 'STOP'   : test.finished =  True; test.save()
    if action == 'DELETE' : test.deleted  =  True; test.save()

    if action == 'MODIFY':
        test.priority = int(request.POST['priority'])
        test.throughput = max(1, int(request.POST['throughput']))
        test.save()

    action += " P={0} TP={1}".format(test.priority, test.throughput)
    LogEvent.objects.create(data=action, author=user.username, test=test)
    return django.http.HttpResponseRedirect('/index/')

def newTest(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return the HTML template for creating a new test when the User  #
    #         is both logged in, and enabled. Otherwise, we redirect those    #
    #         requests to either login, or the index where they are told that #
    #         their account has not yet been enabled                          #
    #                                                                         #
    #  POST : Enabled Users may create new tests. Fields are error checked.   #
    #         If an error is found, the creation is aborted and the list of   #
    #         errors is prestented back to the User on the homepage           #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    if not request.user.is_authenticated:
        return django.http.HttpResponseRedirect('/login/')

    if not Profile.objects.get(user=request.user).enabled:
        return django.http.HttpResponseRedirect('/index/')

    if request.method == 'GET':
        return render(request, 'newTest.html')

    test, errors = OpenBench.utils.createNewTest(request)
    if errors != [] and errors != None:
        errors = ["[{0}]: {1}".format(i, e) for i, e in enumerate(errors)]
        longest = max([len(e) for e in errors])
        errors = ["{0}{1}".format(e, ' ' * (longest-len(e))) for e in errors]
        return index(request, error='\n'.join(errors))

    username = request.user.username
    LogEvent.objects.create(data="CREATE", author=username, test=test)
    return django.http.HttpResponseRedirect('/index/')


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                              CLIENT HOOK VIEWS                              #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

@not_minified_response
def clientGetFiles(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return a URL pointing to the location of Cutechess-cli, as well #
    #         as any DLLs or Shared Object files needed for Cutechess-cli     #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    return HttpResponse(OpenBench.config.OPENBENCH_CONFIG['corefiles'])

@not_minified_response
def clientGetBuildInfo(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  GET  : Return a Dictionary of all of the Engines that are present in   #
    #         config.py, as well as the required compilation tools for them   #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    data = {} # Return all engine-compiler information
    for engine, config in OpenBench.config.OPENBENCH_CONFIG['engines'].items():
        data[engine] = config['build']['compilers']
    return HttpResponse(str(data))

@csrf_exempt
@not_minified_response
def clientGetWorkload(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  POST : Return a Dictionary of data in order to complete a workload. If #
    #         there are no tests for the User, 'None' will be returned. If we #
    #         cannot authenticate the User, 'Bad Credentials' is returned. If #
    #         the posted Machine does not belong the the User, 'Bad Machine'  #
    #         is returned.                                                    #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    # Verify the User's credentials
    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])
    if user == None: return HttpResponse('Bad Credentials')

    # getWorkload() will verify the integrity of the request
    return HttpResponse(OpenBench.utils.getWorkload(user, request))

@csrf_exempt
@not_minified_response
def clientWrongBench(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  POST : Inform the server that an Engine reported an incorrect Bench    #
    #         value during the init process for a Test. We stop the Test and  #
    #         log an Error into the Events table to indicate what happened    #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    # Verify the User's credentials
    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])
    if user == None: return HttpResponse('Bad Credentials')

    # Verify the Machine belongs to the User
    machine = Machine.objects.get(id=int(request.POST['machineid']))
    if machine.user != user: return HttpResponse('Bad Machine')

    # Find and stop the test with the bad bench
    if int(request.POST['wrong']) != 0:
        test = Test.objects.get(id=int(request.POST['testid']))
        test.finished = True; test.save()

    # Collect information on the Error
    wrong   = request.POST['wrong']
    correct = request.POST['correct']
    name    = request.POST['engine']

    # Format a nice Error message
    message = 'Got {0} Expected {1} for {2}'
    message = message.format(wrong, correct, name)

    # Log the error into the Events table
    LogEvent.objects.create(
        test   = test,
        data   = message,
        author = user.username)

    return HttpResponse('None')

@csrf_exempt
@not_minified_response
def clientSubmitNPS(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  POST : Report the speed of the engines in the currently running Test   #
    #         for the User and his Machine. We save this value to display     #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    # Verify the User's credentials
    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])
    if user == None: return HttpResponse('Bad Credentials')

    # Verify the Machine belongs to the User
    machine = Machine.objects.get(id=int(request.POST['machineid']))
    if machine.user != user: return HttpResponse('Bad Machine')

    # Update the NPS and return 'None' to signal no errors
    machine.mnps = float(request.POST['nps']) / 1e6; machine.save()
    return HttpResponse('None')

@csrf_exempt
@not_minified_response
def clientSubmitError(request):

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    #                                                                         #
    #  POST : Report en Engine error to the server. This could be a crash, a  #
    #         timeloss, a disconnect, or an illegal move. Log the Error into  #
    #         the Events table.                                                #
    #                                                                         #
    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    # Verify the User's credentials
    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])
    if user == None: return HttpResponse('Bad Credentials')

    # Verify the Machine belongs to the User
    machine = Machine.objects.get(id=int(request.POST['machineid']))
    if machine.user != user: return HttpResponse('Bad Machine')

    # Flag the Test as having an error
    test = Test.objects.get(id=int(request.POST['testid']))
    test.error = True; test.save()

    # Log the Error into the Events table
    LogEvent.objects.create(
        test   = test,
        author = user.username,
        data   = request.POST['error'])

    return HttpResponse('None')

@csrf_exempt
@not_minified_response
def clientSubmitResults(request):

    # Verify the User's credentials
    user = django.contrib.auth.authenticate(
        username=request.POST['username'],
        password=request.POST['password'])
    if user == None: return HttpResponse('Bad Credentials')

    # Verify the Machine belongs to the User
    machine = Machine.objects.get(id=int(request.POST['machineid']))
    if machine.user != user: return HttpResponse('Bad Machine')

    # updateTest() will return 'None' or 'Stop'
    return HttpResponse(OpenBench.utils.updateTest(request, user))