#!/usr/bin/python3

"""
Run a server in the current directory, or the first parent containing a .serverc file

The .serverc file contains settings:

    command = <command to run> (default: python3 -m http.server)
    cwd = <working directory to run the command in> (default: .)
    port = <port the server runs on (default: chosen at random from available ports)
    env = a list of environment variables to set, separated by spaces
    open = should a page be opened in the browser?
    open_url = the relative url to open in the browser
    delay = the number of seconds to wait before opening the browser

Once the server is running, a browser is opened.
"""

import argparse
import configparser
from contextlib import closing
import hashlib
from multiprocessing import Process
import os
from pathlib import Path
import socket
import subprocess
import sys
from time import sleep

try:
    from watchmake import WatchMaker
except ImportError as e:
    print(e)
    WatchMaker = None

basic_server_cmd = 'python3 /home/christian/bin/httpserver.py'

def port_is_in_use(port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        return s.connect_ex(('localhost', port)) == 0


class ServerRunner(object):
    cwd = '.'
    cmd = None
    open = True
    open_url = '/'
    env_str = ''
    delay = 5

    def __init__(self,options):
        self.options = options

        d = Path('.').resolve()
        got_rc = False
        while d.parent!=d:
            if (d / '.serverc').exists():
                got_rc = True
                break
            d = d.parent

        if not got_rc:
            d = Path('.').resolve()

        self.d = d

        self.rcname = self.d / '.serverc'

    def find_free_port(self):
        h = hashlib.sha256(str(self.d).encode('utf-8')).digest()
        port = 0
        for c in h:
            port = (port*256 + c) % 5000
        port += 2000
        while port_is_in_use(port):
            port += 1
        return port

    def load_config(self):
        self.port = self.find_free_port()

        self.open = self.options.open

        self.config = configparser.ConfigParser()
        if self.rcname.exists():
            with open(self.rcname) as f:
                s = f.read()
                if not s.startswith('['):
                    s = '[settings]\n'+s

            self.config.read_string(s)
            if not self.config.has_section('settings'):
                self.config.add_section('settings')

            self.settings = self.config['settings']
            self.cmd = self.settings.get('command')
            self.cwd = self.settings.get('cwd', self.cwd)
            self.port = self.settings.getint('port',self.port)
            self.env_str = self.settings.get('env',self.env_str)
            self.open = self.settings.getboolean('open', self.open)
            self.open_url = self.settings.get('open_url',self.open_url)
            self.delay = self.settings.getfloat('delay',self.delay)
        else:
            self.config.add_section('settings')

        if self.options.port is not None:
            self.port = self.options.port

        if self.cmd is None:
            self.cmd = basic_server_cmd
            self.delay = 0
            self.cmd += f' {self.port}'
        else:
            self.cmd = self.cmd.format(port=self.port)

    def run(self):
        self.env = os.environ.copy()
        self.env.update({k:v for k,v in [b.split('=') for b in self.env_str.split(' ') if b]})
        self.env['PORT'] = str(self.port)

        self.run_server()
        self.run_watchmaker()
        
        if self.open:
            self.open_browser()

        try:
            self.server_process.wait()
        except KeyboardInterrupt:
            pass

        if self.watchmaker_process:
            self.watchmaker_process.terminate()

    def run_server(self):
        os.chdir(self.d)

        self.server_process = subprocess.Popen(self.cmd.format(**self.env).split(' '), env=self.env, cwd=self.cwd)


    def run_watchmaker(self):
        def target(d):
            try:
                wm = WatchMaker(d)
                wm.run()
            except Exception as e:
                print(e)
                pass

        if WatchMaker is not None and (self.d / '.watchmakerc').exists():
            self.watchmaker_process = Process(target=target,args=(self.d,))
            self.watchmaker_process.start()
        else:
            print("No watchmake")
            print(WatchMaker)
            print((self.d / '.watchmakerc'))
            self.watchmaker_process = None

    def get_full_open_url(self):
        open_url = self.open_url
        hostname = socket.gethostname()
        if not open_url.startswith('/'):
            open_url = '/' + open_url
        full_open_url = f'http://{hostname}.local:{self.port}{open_url}'
        return full_open_url

    def open_browser(self):
        sleep(self.delay)
        full_open_url = self.get_full_open_url()

        print("Opening",full_open_url)
        subprocess.run(['xdg-open',full_open_url])

    def set_config(self,values):
        for line in values:
            name,value = line.split('=')
            self.config.set('settings',name,value)
        with open(self.rcname,'w') as f:
            self.config.write(f)

    def show(self):
        print(self.get_full_open_url())

parser = argparse.ArgumentParser(description='Start a server here.')
parser.add_argument('--port',nargs=1,default=None,type=int, help="Override the port number")
parser.add_argument('-n', '--noopen',action='store_false',dest='open', help="Don't open in the browser")
parser.add_argument('--set',nargs='*', help='Set config options in .serverc')
parser.add_argument('--show', action='store_true', help='Print the URL of this server')
options = parser.parse_args()

runner = ServerRunner(options)
runner.load_config()

if options.set:
    print('set',options.set)
    runner.set_config(options.set)
elif options.show:
    runner.show()
else:
    runner.run()
