# Copyright (c) 2020, Matthew Waters <matthew@centricular.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This program 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.

import inspect
import os
import sys
import shutil
import itertools

import tempfile

from launcher.baseclasses import TestsManager, GstValidateTest, ScenarioManager
from launcher.utils import DEFAULT_TIMEOUT

DEFAULT_BROWSERS = ['firefox', 'chrome']

# list of scenarios. These are the names of the actual scenario files stored
# on disk.
DEFAULT_SCENARIOS = [
        "offer_answer",
        "vp8_send_stream",
        "open_data_channel",
        "send_data_channel_string",
    ]

# various configuration changes that are included from other scenarios.
# key is the name of the override used in the name of the test
# value is the subdirectory where the override is placed
# changes some things about the test like:
# - who initiates the negotiation
# - bundle settings
SCENARIO_OVERRIDES = {
    # name : directory

    # who starts the negotiation
    'local' : 'local_initiates_negotiation',
    'remote' : 'remote_initiates_negotiation',

    # bundle-policy configuration
    # XXX: webrtcbin's bundle-policy=none is not part of the spec
    'none_compat' : 'bundle_local_none_remote_max_compat',
    'none_balanced' : 'bundle_local_none_remote_balanced',
    'none_bundle' : 'bundle_local_none_remote_max_bundle',
    'compat_compat' : 'bundle_local_max_compat_remote_max_compat',
    'compat_balanced' : 'bundle_local_max_compat_remote_balanced',
    'compat_bundle' : 'bundle_local_max_compat_remote_max_bundle',
    'balanced_compat' : 'bundle_local_balanced_remote_max_compat',
    'balanced_balanced' : 'bundle_local_balanced_remote_balanced',
    'balanced_bundle' : 'bundle_local_balanced_remote_bundle',
    'bundle_compat' : 'bundle_local_max_bundle_remote_max_compat',
    'bundle_balanced' : 'bundle_local_max_bundle_remote_balanced',
    'bundle_bundle' : 'bundle_local_max_bundle_remote_max_bundle',
}

bundle_options = ['compat', 'balanced', 'bundle']

# Given an override, these are the choices to choose from.  Each choice is a
# separate test
OVERRIDE_CHOICES = {
    'initiator' : ['local', 'remote'],
    'bundle' : ['_'.join(opt) for opt in itertools.product(['none'] + bundle_options, bundle_options)],
}

# Which scenarios support which override.  All the overrides will be chosen
SCENARIO_OVERRIDES_SUPPORTED = {
    "offer_answer" : ['initiator', 'bundle'],
    "vp8_send_stream" : ['initiator', 'bundle'],
    "open_data_channel" : ['initiator', 'bundle'],
    "send_data_channel_string" : ['initiator', 'bundle'],
}

# Things that don't work for some reason or another.
DEFAULT_BLACKLIST = [
    (r"webrtc\.firefox\.local\..*offer_answer",
     "Firefox doesn't like a SDP without any media"),
    (r"webrtc.*remote.*vp8_send_stream",
     "We can't match payload types with a remote offer and a sending stream"),
    (r"webrtc.*\.balanced_.*",
     "webrtcbin doesn't implement bundle-policy=balanced"),
    (r"webrtc.*\.none_bundle.*",
     "Browsers want a BUNDLE group if in max-bundle mode"),
]

class MutableInt(object):
    def __init__(self, value):
        self.value = value

class GstWebRTCTest(GstValidateTest):
    __used_ports = set()
    __last_id = MutableInt(10)

    @classmethod
    def __get_open_port(cls):
        while True:
            # hackish trick from
            # http://stackoverflow.com/questions/2838244/get-open-tcp-port-in-python?answertab=votes#tab-top
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.bind(("", 0))
            port = s.getsockname()[1]
            if port not in cls.__used_ports:
                cls.__used_ports.add(port)
                s.close()
                return port

            s.close()

    @classmethod
    def __get_available_peer_id(cls):
        # each connection uses two peer ids
        peerid = cls.__last_id.value
        cls.__last_id.value += 2
        return peerid

    def __init__(self, classname, tests_manager, scenario, browser, scenario_override_includes=None, timeout=DEFAULT_TIMEOUT):
        super().__init__("python3",
                        classname,
                        tests_manager.options,
                        tests_manager.reporter,
                        timeout=timeout,
                        scenario=scenario)
        self.webrtc_server = None
        filename = inspect.getframeinfo (inspect.currentframe ()).filename
        self.current_file_path = os.path.dirname (os.path.abspath (filename))
        self.certdir = None
        self.browser = browser
        self.scenario_override_includes = scenario_override_includes

    def launch_server(self):
        if self.options.redirect_logs == 'stdout':
            self.webrtcserver_logs = sys.stdout
        elif self.options.redirect_logs == 'stderr':
            self.webrtcserver_logs = sys.stderr
        else:
            self.webrtcserver_logs = open(self.logfile + '_webrtcserver.log', 'w+')
            self.extra_logfiles.add(self.webrtcserver_logs.name)

        generate_certs_location = os.path.join(self.current_file_path, "..", "..", "..", "signalling", "generate_cert.sh")
        self.certdir = tempfile.mkdtemp()
        command = [generate_certs_location, self.certdir]

        server_env = os.environ.copy()

        subprocess.run(command,
                         stderr=self.webrtcserver_logs,
                         stdout=self.webrtcserver_logs,
                         env=server_env)

        self.server_port = self.__get_open_port()

        server_location = os.path.join(self.current_file_path, "..", "..", "..", "signalling", "simple_server.py")
        command = [server_location, "--cert-path", self.certdir, "--addr", "127.0.0.1", "--port", str(self.server_port)]

        self.webrtc_server = subprocess.Popen(command,
                                              stderr=self.webrtcserver_logs,
                                              stdout=self.webrtcserver_logs,
                                              env=server_env)
        while True:
            s = socket.socket()
            try:
                s.connect((("127.0.0.1", self.server_port)))
                break
            except ConnectionRefusedError:
                time.sleep(0.1)
                continue
            finally:
                s.close()

        return ' '.join(command)

    def build_arguments(self):
        gst_id = self.__get_available_peer_id()
        web_id = gst_id + 1

        self.add_arguments(os.path.join(self.current_file_path, '..', 'webrtc_validate.py'))
        self.add_arguments('--server')
        self.add_arguments("wss://127.0.0.1:%s" % (self.server_port,))
        self.add_arguments('--browser')
        self.add_arguments(self.browser)
        self.add_arguments("--html-source")
        html_page = os.path.join(self.current_file_path, '..', 'web', 'single_stream.html')
        html_params = '?server=127.0.0.1&port=' + str(self.server_port) + '&id=' + str(web_id)
        self.add_arguments("file://" + html_page + html_params)
        self.add_arguments("--name")
        self.add_arguments(self.classname)
        self.add_arguments('--peer-id')
        self.add_arguments(str(web_id))
        self.add_arguments(str(gst_id))

    def close_logfile(self):
        super().close_logfile()
        if not self.options.redirect_logs:
            self.webrtcserver_logs.close()

    def process_update(self):
        res = super().process_update()
        if res:
            kill_subprocess(self, self.webrtc_server, DEFAULT_TIMEOUT)
            self.__used_ports.remove(self.server_port)
            if self.certdir:
                shutil.rmtree(self.certdir, ignore_errors=True)

        return res

    def get_subproc_env(self):
        env = super().get_subproc_env()
        if not self.scenario_override_includes:
            return env

        # this feels gross...
        paths = env.get('GST_VALIDATE_SCENARIOS_PATH', '').split(os.pathsep)
        new_paths = []
        for p in paths:
            new_paths.append(p)
            for override_path in self.scenario_override_includes:
                new_p = os.path.join(p, override_path)
                if os.path.exists (new_p):
                    new_paths.append(new_p)
        env['GST_VALIDATE_SCENARIOS_PATH'] = os.pathsep.join(new_paths)

        return env

class GstWebRTCTestsManager(TestsManager):
    scenarios_manager = ScenarioManager()
    name = "webrtc"

    def __init__(self):
        super(GstWebRTCTestsManager, self).__init__()
        self.loading_testsuite = self.name
        self._scenarios = []

    def add_scenarios(self, scenarios):
        if isinstance(scenarios, list):
            self._scenarios.extend(scenarios)
        else:
            self._scenarios.append(scenarios)

        self._scenarios = list(set(self._scenarios))

    def set_scenarios(self, scenarios):
        self._scenarios = []
        self.add_scenarios(scenarios)

    def get_scenarios(self):
        return self._scenarios

    def populate_testsuite(self):
        self.add_scenarios (DEFAULT_SCENARIOS)
        self.set_default_blacklist(DEFAULT_BLACKLIST)

    def list_tests(self):
        if self.tests:
            return self.tests

        scenarios = [(scenario_name, self.scenarios_manager.get_scenario(scenario_name))
                     for scenario_name in self.get_scenarios()]

        for browser in DEFAULT_BROWSERS:
            for name, scenario in scenarios:
                if not scenario:
                    self.warning("Could not find scenario %s" % name)
                    continue
                if not SCENARIO_OVERRIDES_SUPPORTED[name]:
                    # no override choices supported
                    classname = browser + '.' + name
                    print ("adding", classname)
                    self.add_test(GstWebRTCTest(classname, self, scenario, browser))
                else:
                    for overrides in itertools.product(*[OVERRIDE_CHOICES[c] for c in SCENARIO_OVERRIDES_SUPPORTED[name]]):
                        oname = '.'.join (overrides)
                        opaths = [SCENARIO_OVERRIDES[p] for p in overrides]
                        classname = browser + '.' + oname + '.' + name
                        print ("adding", classname)
                        self.add_test(GstWebRTCTest(classname, self, scenario, browser, opaths))

        return self.tests