#!/usr/bin/env python

import json
import multiprocessing
import os
import random
import threading
import webbrowser

# third-party imports
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.httpclient
import tornado.gen

# local imports
import settings
import LikedSavedDatabase
from downloaders import redditUserImageScraper
from utils import utilities

# Require a username and password in order to use the web interface. See ReadMe.org for details.
#enable_authentication = False
enable_authentication = True

useSSL = True

if enable_authentication:
    import PasswordManager

# List of valid user ids (used to compare user cookie)
authenticated_users = []

# If "next" isn't specified from login, redirect here after login instead
landingPage = "/"

class SessionData:
    def __init__(self):
        # Just in case, because tornado is multithreaded
        self.lock = threading.Lock()
        
        self.randomHistory = []
        self.randomHistoryIndex = -1
        self.favorites = []
        self.favoritesIndex = 0
        self.currentImage = None
        self.randomImageFilter = ''
        self.filteredImagesCache = []
        
        self.currentDirectoryPath = ''
        self.currentDirectoryCache = []
        self.directoryFilter = ''

    def acquire(self):
        self.lock.acquire()
    def release(self):
        self.lock.release()

# user id : session data
userSessionData = {}

videoExtensions = ('.mp4', '.webm')
supportedExtensions = ('.gif', '.jpg', '.jpeg', '.png', '.mp4', '.webm', '.riff')

savedImagesCache = []
def generateSavedImagesCache(outputDir):
    global savedImagesCache
    # Clear cache in case already created
    savedImagesCache = []

    print('Creating content cache...', flush=True)
    
    for root, dirs, files in os.walk(outputDir):
        for file in files:
            if file.endswith(supportedExtensions):
                savedImagesCache.append(os.path.join(root, file))

    print('Finished creating content cache ({} images/videos)'.format(len(savedImagesCache)))

def getRandomImage(filteredImagesCache=None, randomImageFilter=''):
    if not savedImagesCache:
        generateSavedImagesCache(settings.settings['Output_dir'])

    if filteredImagesCache:
        randomImage = random.choice(filteredImagesCache)
    else:
        randomImage = random.choice(savedImagesCache)

    print('\tgetRandomImage(): Chose random image {} (filter {})'.format(randomImage, randomImageFilter))

    serverPath = utilities.outputPathToServerPath(randomImage)

    return randomImage, serverPath

#
# Tornado handlers
#

# See https://github.com/tornadoweb/tornado/blob/stable/demos/blog/blog.py
# https://www.tornadoweb.org/en/stable/guide/security.html

def login_get_current_user(handler):
    if enable_authentication:
        cookie = handler.get_secure_cookie("user")
        if cookie in authenticated_users:
            return cookie
        else:
            print("Bad/expired cookie received")
            return None
    else:
        return "authentication_disabled"

class AuthHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return login_get_current_user(self)
    
class LoginHandler(AuthHandler):
    def get(self):
        if not enable_authentication:
            self.redirect("/")
        else:
            if PasswordManager.havePasswordsBeenSet():
                self.render("templates/Login.html",
                            next=self.get_argument("next", landingPage),
                            xsrf_form_html=self.xsrf_form_html())
            else:
                # New password setup
                self.render("templates/LoginCreate.html",
                            next=self.get_argument("next", landingPage),
                            xsrf_form_html=self.xsrf_form_html())

    def post(self):
        global authenticated_users
        # Test password
        print("Attempting to authorize user {}...".format(self.get_argument("name")))
        if enable_authentication and PasswordManager.verify(self.get_argument("password")):
            # Generate new authenticated user session
            randomGenerator = random.SystemRandom()
            cookieSecret = str(randomGenerator.getrandbits(128))
            authenticated_user = self.get_argument("name") + "_" + cookieSecret
            authenticated_user = authenticated_user.encode()
            authenticated_users.append(authenticated_user)
            
            # Set the cookie on the user's side
            self.set_secure_cookie("user", authenticated_user)
            
            print("Authenticated user {}".format(self.get_argument("name")))
            
            # Let them in
            self.redirect(self.get_argument("next", landingPage))
        else:
            print("Refused user {} (password doesn't match any in database)".format(self.get_argument("name")))
            self.redirect("/login")
            
class LogoutHandler(AuthHandler):
    @tornado.web.authenticated
    def get(self):
        global authenticated_users
        
        if enable_authentication:
            print("User {} logging out".format(self.current_user))
            if self.current_user in authenticated_users:
                authenticated_users.remove(self.current_user)
            self.redirect("/login")
        else:
            self.redirect("/")

class SetPasswordHandler(AuthHandler):
    def get(self):
        pass

    def post(self):
        if not enable_authentication:
            self.redirect("/")
        else:
            print("Attempting to set password")
            if PasswordManager.havePasswordsBeenSet():
                print("Rejected: Password has already been set!")
            elif self.get_argument("password") != self.get_argument("password_verify"):
                print("Rejected: password doesn't match verify field!")
            else:
                PasswordManager.createPassword(self.get_argument("password"))
                print("Success: Set password")

            self.redirect("/login")

class AuthedStaticHandler(tornado.web.StaticFileHandler):
    def get_current_user(self):
        return login_get_current_user(self)
    
    @tornado.web.authenticated
    def prepare(self):
        pass

class HomeHandler(AuthHandler):
    @tornado.web.authenticated
    def get(self):
        self.render('webInterface/index.html')

def settingsToHtmlForm():
    settingsInputs = []

    for sectionSettingsPair in settings.settingsStructure:
        settingsInputs.append('<h2>{}</h2>'.format(sectionSettingsPair[0]))

        for sectionOption in sectionSettingsPair[1]:
            option = None
            optionComment = ''
            if type(sectionOption) == tuple:
                option = sectionOption[0]
                optionComment = '<p class="optionComment">{}</p>'.format(sectionOption[1])
            else:
                option = sectionOption

            if type(settings.settings[option]) == bool:
                settingsInputs.append('''<input type="checkbox" id="{option}" name="{option}" value="{optionValue}" {checkedState} />
                                      <label for="{option}">{optionName}</label>{comment}
                                      <br />'''
                                      .format(option=option, optionName=option.replace('_', ' '),
                                              comment=optionComment,
                                              checkedState=('checked' if settings.settings[option] else ''),
                                              optionValue=('1' if settings.settings[option] else '0')))

            elif type(settings.settings[option]) == int:
                settingsInputs.append('''<label for="{option}">{optionName}</label>
                                     <input type="number" id="{option}" name="{option}" value="{optionValue}" />{comment}
                                     <br />'''
                                      .format(option=option, optionName=option.replace('_', ' '), comment=optionComment,
                                              optionValue=settings.settings[option]))

            elif type(settings.settings[option]) == str:
                settingsInputs.append('''<label for="{option}">{optionName}</label>
                                     <input type="{type}" id="{option}" name="{option}" value="{optionValue}" />{comment}
                                     <br />'''
                                      .format(option=option, optionName=option.replace('_', ' '),
                                              comment=optionComment, optionValue=settings.settings[option],
                                              type=('password' if 'secret' in option.lower() or 'password' in option.lower() else 'text')))

    return ''.join(settingsInputs)

unsupportedSubmissionShownColumns = ['title',
                                     'bodyUrl',
                                     'reasonForFailure']
unsupportedSubmissionColumnLabels = ['Retry', 'Source', 'Title',
                                     'Content URL',
                                     'Reason for Failure']
class UnsupportedSubmissionsHandler(AuthHandler):
    def unsupportedSubmissionToTableColumns(self, unsupportedSubmission):
        rowHtml = ''

        rowHtml += '\t<td><input type="checkbox" name="shouldRetry" value="{}"/></td>\n'.format(unsupportedSubmission['id'])

        # Special case source cell
        rowHtml += '\t<td><a href="{}">{}</a></td>\n'.format(
            'https://reddit.com{}'.format(unsupportedSubmission['postUrl']) if unsupportedSubmission['source'] == 'reddit'
            else unsupportedSubmission['postUrl'],
            unsupportedSubmission['source'])
        
        for columnName in unsupportedSubmissionShownColumns:
            if 'url' in columnName[-3:].lower():
                rowHtml += '\t<td><a href="{}">Content</a></td>\n'.format(unsupportedSubmission['bodyUrl'])
            else:
                rowHtml += '\t<td>{}</td>\n'.format(unsupportedSubmission[columnName])
        return rowHtml

    def createTableHeader(self):
        tableHeaderHtml = '<thead>\n<tr class="header">\n'

        for columnName in unsupportedSubmissionColumnLabels:
            tableHeaderHtml +='<th>{}</th>'.format(columnName)

        tableHeaderHtml += '</tr>\n</thead>\n<tbody>\n'
        return tableHeaderHtml

    def getPendingFixups(self):
        fixupHtml = ''

        missingPixivSubmissions = LikedSavedDatabase.db.getMissingPixivSubmissionIds()
        if len(missingPixivSubmissions):
            if not fixupHtml:
                fixupHtml += "<h2>Download missing content</h2>"
            fixupHtml += '<p>There was an error which caused {} Pixiv submissions to not be downloaded.</p>'.format(len(missingPixivSubmissions))
            fixupHtml += '<button id="FixupPixiv" onclick="fixupPixiv()">Download missing Pixiv submissions</button>'
            fixupHtml += '<p>You should only need to do this once. The code error has been fixed.</p>'

        return fixupHtml

    @tornado.web.authenticated
    def get(self):
        unsupportedSubmissionsListHtml = self.createTableHeader()
        unsupportedSubmissions = LikedSavedDatabase.db.getAllUnsupportedSubmissions()
        i = 0
        for unsupportedSubmission in reversed(unsupportedSubmissions):
            unsupportedSubmissionsListHtml += ('<tr class="{}">{}</tr>\n'
                                               .format('even' if i % 2 == 0 else 'odd',
                                                       self.unsupportedSubmissionToTableColumns(unsupportedSubmission)))
            i += 1

        unsupportedSubmissionsListHtml += '</tbody>\n'

        self.render("templates/UnsupportedSubmissions.html",
                    unsupported_submissions_html=unsupportedSubmissionsListHtml,
                    length_unsupported_submissions=len(unsupportedSubmissions),
                    fixup_html=self.getPendingFixups())

class SettingsHandler(AuthHandler):
    def doSettings(self, afterSubmit):
        htmlSettingsForm = settingsToHtmlForm()
        settingsFilename = settings.getSettingsFilename()

        self.render("templates/Settings.html",
                    status_html=('<p><b>Settings updated</b></p>' if afterSubmit else ''),
                    settings_filename=settingsFilename,
                    settings_form_html=htmlSettingsForm,
                    xsrf_form_html=self.xsrf_form_html())

    @tornado.web.authenticated
    def get(self):
        self.doSettings(False)

    @tornado.web.authenticated
    def post(self):
        currentOutputDir = settings.settings['Output_dir']
        
        print('Received new settings')
        
        for option in settings.settings:
            newValue = self.get_argument(option, None)
            if not newValue:
                # It's okay if it's a boolean because POST doesn't send unchecked checkboxes
                # This means the user set the value to false
                if type(settings.settings[option]) == bool:
                    settings.settings[option] = False
                else:
                    print('Warning: Option {} unset! The settingsStructure might be out of sync.'
                          '\n\tIgnore this if the field is intentionally empty'.format(option))
            else:
                # All false bools are handed in the above if block, so we know they're true here
                if type(settings.settings[option]) == bool:
                    newValue = True
                elif type(settings.settings[option]) == int:
                    newValue = int(newValue)
                
                settings.settings[option] = newValue
                # print('\tSet {} = {}'.format(option, newValue))

        # Write out the new settings
        settings.writeServerSettings()
        
        # Respond with a settings page saying we've updated the settings
        self.doSettings(True)

        # Refresh the cache in case the output directory changed
        if currentOutputDir != settings.settings['Output_dir']:
            generateSavedImagesCache(settings.settings['Output_dir'])
            

class RandomImageBrowserWebSocket(tornado.websocket.WebSocketHandler):
    connections = set()

    def cacheFilteredImages(self):
        # Clear the cache
        self.sessionData.filteredImagesCache = []

        if not self.sessionData.randomImageFilter:
            return

        randomImageFilterLower = self.sessionData.randomImageFilter.lower()
    
        for imagePath in savedImagesCache:
            if randomImageFilterLower in imagePath.lower():
                self.sessionData.filteredImagesCache.append(imagePath)

        print('\tFiltered images with "{}"; {} images matching filter'
              .format(self.sessionData.randomImageFilter,
                      len(self.sessionData.filteredImagesCache)))

    def changeCurrentDirectory(self, newDirectory):
        self.sessionData.currentDirectoryPath = newDirectory
        dirList = os.listdir(self.sessionData.currentDirectoryPath)
        
        filteredDirList = []
        for fileOrDir in dirList:
            # The script spits out a lot of .json files the user probably doesn't want to see
            if (not fileOrDir.endswith('.json')
                and (not self.sessionData.directoryFilter
                     or self.sessionData.directoryFilter.lower() in fileOrDir.lower())):
                filteredDirList.append(fileOrDir)
                
        self.sessionData.currentDirectoryCache = sorted(filteredDirList)

    def open(self):
        global userSessionData
        currentUser = login_get_current_user(self)
        if not currentUser:
            # Failed authorization
            return None
        
        self.connections.add(self)
        
        if currentUser not in userSessionData:
            newSessionData = SessionData()
            userSessionData[currentUser] = newSessionData

        self.sessionData = userSessionData[currentUser]

        self.sessionData.acquire()
        # Set up the directory cache with the top-level output
        self.changeCurrentDirectory(settings.settings['Output_dir'])
        self.sessionData.release()
            
    def on_message(self, message):
        currentUser = login_get_current_user(self)
        if not currentUser:
            # Failed authorization
            return None
        
        print('RandomImageBrowserWebSocket: Received message ', message)
        parsedMessage = json.loads(message)
        command = parsedMessage['command']
        print('RandomImageBrowserWebSocket: Command ', command)
        action = ''

        self.sessionData.acquire()
        
        """
         Random Image Browser
        """
        
        if command == 'imageAddToFavorites':
            if self.sessionData.currentImage:
                self.sessionData.favorites.append(self.sessionData.currentImage)
                self.sessionData.favoritesIndex = len(self.sessionData.favorites) - 1
                LikedSavedDatabase.db.addFileToCollection(self.sessionData.currentImage[1], "Favorites")
                 
        if command == 'nextFavorite':
            self.sessionData.favoritesIndex += 1
            if self.sessionData.favoritesIndex >= 0 and self.sessionData.favoritesIndex < len(self.sessionData.favorites):
                action = 'setImage'
                fullImagePath, serverImagePath = self.sessionData.favorites[self.sessionData.favoritesIndex]
            else:
                self.sessionData.favoritesIndex = len(self.sessionData.favorites) - 1
                if len(self.sessionData.favorites):
                    action = 'setImage'
                    fullImagePath, serverImagePath = self.sessionData.favorites[self.sessionData.favoritesIndex]

        if command == 'previousFavorite' and len(self.sessionData.favorites):
            action = 'setImage'

            if self.sessionData.favoritesIndex > 0:
                self.sessionData.favoritesIndex -= 1
                
            fullImagePath, serverImagePath = self.sessionData.favorites[self.sessionData.favoritesIndex]

        if command == 'nextImage':
            action = 'setImage'

            if self.sessionData.randomHistoryIndex == -1 or self.sessionData.randomHistoryIndex >= len(self.sessionData.randomHistory) - 1:
                fullImagePath, serverImagePath = getRandomImage(self.sessionData.filteredImagesCache, self.sessionData.randomImageFilter)
                self.sessionData.randomHistory.append((fullImagePath, serverImagePath))
                self.sessionData.randomHistoryIndex = len(self.sessionData.randomHistory) - 1
            else:
                self.sessionData.randomHistoryIndex += 1
                fullImagePath, serverImagePath = self.sessionData.randomHistory[self.sessionData.randomHistoryIndex]

        if command == 'previousImage':
            action = 'setImage'

            if self.sessionData.randomHistoryIndex > 0:
                self.sessionData.randomHistoryIndex -= 1
                
            fullImagePath, serverImagePath = self.sessionData.randomHistory[self.sessionData.randomHistoryIndex]

        if command in ['nextImageInFolder', 'previousImageInFolder'] and len(self.sessionData.randomHistory):
            fullImagePath, serverImagePath = self.sessionData.currentImage
                
            folder = fullImagePath[:fullImagePath.rfind('/')]
            imagesInFolder = []
            for root, dirs, files in os.walk(folder):
                for file in files:
                    if file.endswith(supportedExtensions):
                        imagesInFolder.append(os.path.join(root, file))
            utilities.sort_naturally(imagesInFolder)
            currentImageIndex = imagesInFolder.index(fullImagePath)
            if currentImageIndex >= 0:
                action = 'setImage'
                
                nextImageIndex = currentImageIndex + (1 if command == 'nextImageInFolder' else -1)
                if nextImageIndex == len(imagesInFolder):
                    nextImageIndex = 0
                if nextImageIndex < 0:
                    nextImageIndex = len(imagesInFolder) - 1
                    
                fullImagePath = imagesInFolder[nextImageIndex]
                serverImagePath = utilities.outputPathToServerPath(fullImagePath)

        if command == 'setFilter':
            newFilter = parsedMessage['filter']
            if newFilter != self.sessionData.randomImageFilter:
                self.sessionData.randomImageFilter = newFilter
                self.cacheFilteredImages()

        """
         Directory browser
        """

        if command == 'setDirectoryFilter':
            newFilter = parsedMessage['filter']
            if newFilter != self.sessionData.directoryFilter:
                self.sessionData.directoryFilter = newFilter
                # Refresh cache with new filter
                self.changeCurrentDirectory(self.sessionData.currentDirectoryPath)
                action = 'sendDirectory'

        if command == 'listCurrentDirectory':
            action = 'sendDirectory'

        if command == 'changeDirectory':
            # Reset the filter (chances are the user only wanted to filter at one level
            self.sessionData.directoryFilter = ''
            self.changeCurrentDirectory('{}/{}'.format(self.sessionData.currentDirectoryPath, parsedMessage['path']));
            action = 'sendDirectory'

        if command == 'directoryUp':
            # Don't allow going higher than output dir
            if self.sessionData.currentDirectoryPath != settings.settings['Output_dir']:
                upDirectory = (settings.settings['Output_dir']  +
                               self.sessionData.currentDirectoryPath[len(settings.settings['Output_dir'])
                                                         : self.sessionData.currentDirectoryPath.rfind('/')])
                # Reset the filter (chances are the user only wanted to filter at one level
                self.sessionData.directoryFilter = ''
                self.changeCurrentDirectory(upDirectory)
                action = 'sendDirectory'
            
        if command == 'directoryRoot':
            # Reset the filter (chances are the user only wanted to filter at one level
            self.sessionData.directoryFilter = ''
            self.changeCurrentDirectory(settings.settings['Output_dir'])
            action = 'sendDirectory'

        """
         Actions
        """

        # Only send a response if needed
        if action == 'setImage':
            # Stupid hack
            if serverImagePath.endswith(videoExtensions):
                action = 'setVideo'
                
            self.sessionData.currentImage = (fullImagePath, serverImagePath)
            responseMessage = ('{{"responseToCommand":"{}", "action":"{}", "fullImagePath":"{}", "serverImagePath":"{}"}}'
                               .format(command, action, fullImagePath, serverImagePath))
            self.write_message(responseMessage)

        if action == 'sendDirectory':
            directoryList = ''
            for path in self.sessionData.currentDirectoryCache:
                isSupportedFile = path.endswith(supportedExtensions)
                isFile = '.' in path
                if path.endswith(videoExtensions):
                    fileType = 'video'
                elif isSupportedFile:
                    fileType = 'image'
                elif isFile:
                    fileType = 'file'
                else:
                    fileType = 'dir'
                serverPath = 'output' + self.sessionData.currentDirectoryPath[len(settings.settings['Output_dir']):] + '/' + path
                directoryList += '{{"path":"{}", "type":"{}", "serverPath":"{}"}},'.format(path, fileType, serverPath)

            # Do directoryList[:-1] (yuck) to trim the final trailing comma because JSON doesn't like it
            responseMessage = ('{{"responseToCommand":"{}", "action":"{}", "directoryList":[{}]}}'
                               .format(command, action, directoryList[:-1]))
            self.write_message(responseMessage)

        self.sessionData.release()
    

    def on_close(self):
        self.connections.remove(self)

scriptPipeConnection = None
scriptProcess = None

def startScript(functionToRun, args=None):
    global scriptPipeConnection, scriptProcess
    
    # Script already running
    if scriptProcess and scriptProcess.is_alive():
        return
    
    scriptPipeConnection, childConnection = multiprocessing.Pipe()
    if not args:
        scriptProcess = multiprocessing.Process(target=functionToRun,
                                                args=(childConnection,))
    else:
        scriptProcess = multiprocessing.Process(target=functionToRun,
                                                args=(childConnection, args,))
    scriptProcess.start()

runScriptWebSocketConnections = set()
class RunScriptWebSocket(tornado.websocket.WebSocketHandler):
    def open(self):
        if not login_get_current_user(self):
            return None
        
        global runScriptWebSocketConnections
        runScriptWebSocketConnections.add(self)

    def on_message(self, message):
        if not login_get_current_user(self):
            return None
        
        print('RunScriptWebSocket: Received message ', message)

        parsedMessage = json.loads(message)
        command = parsedMessage['command']

        print('RunScriptWebSocket: Command ', command)

        if scriptProcess and scriptProcess.is_alive():
            print('RunScriptWebSocket: Script already running')
            responseMessage = ('{{"message":"{}", "action":"{}"}}'
                               .format('A download process is already running. Please wait until it completes.\\n',
                                       'printMessage'))
            self.write_message(responseMessage)
        
        if command == 'runScript':
            print('RunScriptWebSocket: Starting script')

            startScript(redditUserImageScraper.runLikedSavedDownloader)
                
            responseMessage = ('{{"message":"{}", "action":"{}"}}'
                               .format('Running downloader.\\n', 'printMessage'))
            self.write_message(responseMessage)
        elif command == 'retrySubmissions':
            print('RunScriptWebSocket: Starting script')
            if parsedMessage['submissionsToRetry']:
                submissionIds = []
                for submissionId in parsedMessage['submissionsToRetry']:
                    submissionIds.append(int(submissionId))

                startScript(redditUserImageScraper.saveRequestedSubmissions,
                            submissionIds)
                
                responseMessage = ('{{"message":"{}", "action":"{}"}}'
                                   .format('Running downloader.\\n', 'printMessage'))
                self.write_message(responseMessage)
            else:
                responseMessage = ('{{"message":"{}", "action":"{}"}}'
                                   .format('No content selected.\\n', 'printMessage'))
                self.write_message(responseMessage)
        # Fix the non-unique filenames error
        elif command == 'fixupPixivSubmissions':
            print('RunScriptWebSocket: Starting pixiv fixup')
            missingPixivSubmissions = LikedSavedDatabase.db.getMissingPixivSubmissionIds()
            missingPixivSubmissionIds = []
            for missingPixivSubmission in missingPixivSubmissions:
                missingPixivSubmissionIds.append(int(missingPixivSubmission['id']))

            # print(missingPixivSubmissionIds)
            startScript(redditUserImageScraper.saveRequestedSubmissions, missingPixivSubmissionIds)

            responseMessage = ('{{"message":"{}", "action":"{}"}}'
                               .format('Running downloader to download {} missing pixiv submissions.\\n'
                                       .format(len(missingPixivSubmissions)),
                                       'printMessage'))
        elif command == 'explicitDownloadUrls':
            print('RunScriptWebSocket: Starting script')
            if parsedMessage['urls']:
                urls = []
                urlLines = parsedMessage['urls'].split('\n')
                for line in urlLines:
                    # TODO: It would be a good idea to do some validation here, and maybe even regex extract URLs
                    urls.append(line)

                print(urls)
                startScript(redditUserImageScraper.saveRequestedUrls, urls)
                
                responseMessage = ('{{"message":"{}", "action":"{}"}}'
                                   .format('Running downloader.\\n', 'printMessage'))
                self.write_message(responseMessage)
            else:
                responseMessage = ('{{"message":"{}", "action":"{}"}}'
                                   .format('No URLs provided.\\n', 'printMessage'))
                self.write_message(responseMessage)
        else:
            print('RunScriptWebSocket: Error: Received command not understood')

    def on_close(self):
        global runScriptWebSocketConnections
        runScriptWebSocketConnections.remove(self)

def updateScriptStatus():
    global scriptPipeConnection
    # If no pipe or no data to receive from pipe, we're done
    # Poll() is non-blocking whereas recv is blocking
    try:
        if (not runScriptWebSocketConnections
            or not scriptPipeConnection
            or not scriptPipeConnection.poll()):
            return
    except OSError:
        scriptPipeConnection = None
        return

    try:
        pipeOutput = scriptPipeConnection.recv()
        
        if pipeOutput:
            responseMessage = ('{{"message":"{}", "action":"{}"}}'
                               .format(pipeOutput.replace('\n', '\\n').replace('\t', ''),
                                       'printMessage'))
        
            for client in runScriptWebSocketConnections:
                client.write_message(responseMessage)

            if redditUserImageScraper.scriptFinishedSentinel in pipeOutput:
                # Script finished; refresh image cache
                print('Refreshing cache due to script finishing')
                generateSavedImagesCache(settings.settings['Output_dir'])
                responseMessage = ('{{"action":"{}"}}'
                                   .format('scriptFinished'))
            
                for client in runScriptWebSocketConnections:
                    client.write_message(responseMessage)

                scriptPipeConnection.close()
    except EOFError:
        scriptPipeConnection = None
        print("Lost connection to subprocess!")
        responseMessage = ('{{"message":"{}", "action":"{}"}}'
                           .format("Downloader encountered a problem. Check your server output.",
                                   'printMessage'))
            
        for client in runScriptWebSocketConnections:
            client.write_message(responseMessage)

#
# Startup
#

def make_app():
    # Each time the server starts up, invalidate all cookies
    randomGenerator = random.SystemRandom()
    cookieSecret = str(randomGenerator.getrandbits(128))
    
    return tornado.web.Application([
        # Home page
        (r'/', HomeHandler),
        
        # Login
        (r'/login', LoginHandler),
        (r'/logout', LogoutHandler),
        (r'/setPassword', SetPasswordHandler),
        
        # Configure the script
        (r'/settings', SettingsHandler),

        # Handles messages for run script
        (r'/runScriptWebSocket', RunScriptWebSocket),

        # Handles messages for randomImageBrowser
        (r'/randomImageBrowserWebSocket', RandomImageBrowserWebSocket),

        (r'/unsupportedSubmissions', UnsupportedSubmissionsHandler),
        #
        # Static files
        #
        (r'/webInterface/(.*)', AuthedStaticHandler, {'path' : 'webInterface'}),
        # Don't change this "output" here without changing the other places as well
        (r'/output/(.*)', AuthedStaticHandler, {'path' : settings.settings['Output_dir']}),

        # Files served regardless of whether the user is authenticated. Only login page resources
        # should be in this folder, because anyone can see them
        (r'/webInterfaceNoAuth/(.*)', tornado.web.StaticFileHandler, {'path' : 'webInterfaceNoAuth'}),
    ],
                                   xsrf_cookies=True,
                                   cookie_secret=cookieSecret,
                                   login_url="/login")

if __name__ == '__main__':
    print('Loading settings...')
    settings.getSettings()

    print('Content output directory: ' + settings.settings['Output_dir'])
    if not settings.settings['Output_dir']:
        print('WARNING: No output directory specified! This will probably break things')

    if not savedImagesCache:
        generateSavedImagesCache(settings.settings['Output_dir'])

    LikedSavedDatabase.initializeFromSettings(settings.settings)

    # Backwards compatibility: Read the old .json files into the database. This can be slow for old
    # repositories, so only do it once
    if not settings.settings['Database_Has_Imported_All_Submissions']:
        # Also scan output_dir because Metadata_output_dir was a late addition
        LikedSavedDatabase.importFromAllJsonInDir(settings.settings['Output_dir'])
        LikedSavedDatabase.importFromAllJsonInDir(settings.settings['Metadata_output_dir'])
        settings.settings['Database_Has_Imported_All_Submissions'] = True
        settings.writeServerSettings()
        print('Successfully imported "All" Submissions into database')
    if not settings.settings['Database_Has_Imported_Unsupported_Submissions']:
        LikedSavedDatabase.importUnsupportedSubmissionsFromAllJsonInDir(settings.settings['Output_dir'])
        LikedSavedDatabase.importUnsupportedSubmissionsFromAllJsonInDir(settings.settings['Metadata_output_dir'])
        print('Removing Unsupported Submissions which have file associations')
        LikedSavedDatabase.db.removeUnsupportedSubmissionsWithFileAssociations()
        settings.settings['Database_Has_Imported_Unsupported_Submissions'] = True
        settings.writeServerSettings()
        print('Successfully imported Unsupported Submissions into database')
        # TODO
    # if not settings.settings['Database_Has_Imported_Comments']:
        # LikedSavedDatabase.importFromAllJsonInDir(settings.settings['Output_dir'])
        # settings.settings['Database_Has_Imported_Comments'] = True

    # This isn't pretty, but it'll get the job done
    webSocketSettings = open('webInterface/webSocketSettings.js', 'w')
    webSocketSettings.write('useSSL = {};'.format('true' if useSSL else 'false'))
    webSocketSettings.close()

    port = settings.settings['Port'] if settings.settings['Port'] else 8888
    print('\nStarting Content Collector Server on port {}...'.format(port))
    app = make_app()

    # Generating a self-signing certificate:
    # openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem
    # (from https://jupyter-notebook.readthedocs.io/en/latest/public_server.html)
    # I then had to tell Firefox to trust this certificate even though it is self-signing (because
    # I want a free certificate for this non-serious project)
    if useSSL:
        if os.path.exists("certificates/liked_saved_server.crt.pem"):
            app.listen(port, ssl_options={"certfile":"certificates/liked_saved_server.crt.pem",
                                          "keyfile":"certificates/liked_saved_server.crt.key"})
        # For backwards compatibility
        elif os.path.exists("certificates/server_jupyter_based.crt.pem"):
            app.listen(port, ssl_options={"certfile":"certificates/server_jupyter_based.crt.pem",
                                          "keyfile":"certificates/server_jupyter_based.crt.key"})
        else:
            print('\n\tERROR: Certificates non-existent! Run ./Generate_Certificates.sh to create them')
    else:
        # Show the warning only if SSL is not enabled
        print('\n\tWARNING: Do NOT run this server on the internet (e.g. port-forwarded)'
          ' nor when\n\t connected to an insecure LAN! It is not protected against malicious use.\n')
        app.listen(port)

    if settings.settings['Launch_Browser_On_Startup']:
        browseUrl  ="{}://localhost:{}".format('https' if useSSL else 'http', port)
        print("Attempting to launch user's default browser to {}".format(browseUrl))
        webbrowser.open(browseUrl)

    ioLoop = tornado.ioloop.IOLoop.current()
    updateStatusCallback = tornado.ioloop.PeriodicCallback(updateScriptStatus, 100)
    updateStatusCallback.start()
    ioLoop.start()
