# Standard Library
import subprocess
from gettext import gettext as _

from lutris.exceptions import MissingGameExecutableError

# Lutris Modules
from lutris.runners.runner import Runner
from lutris.util import system
from lutris.util.display import DISPLAY_MANAGER
from lutris.util.joypad import get_controller_mappings
from lutris.util.log import logger

DEFAULT_MEDNAFEN_SCALER = "nn4x"


class mednafen(Runner):
    human_name = _("Mednafen")
    description = _("Multi-system emulator: NES, PC Engine, PSX…")
    platforms = [
        _("Nintendo Game Boy (Color)"),
        _("Nintendo Game Boy Advance"),
        _("Sega Game Gear"),
        _("Sega Genesis/Mega Drive"),
        _("Atari Lynx"),
        _("Sega Master System"),
        _("SNK Neo Geo Pocket (Color)"),
        _("Nintendo NES"),
        _("NEC PC Engine TurboGrafx-16"),
        _("NEC PC-FX"),
        _("Sony PlayStation"),
        _("Sega Saturn"),
        _("Nintendo SNES"),
        _("Bandai WonderSwan"),
        _("Nintendo Virtual Boy"),
    ]
    machine_choices = (
        (_("Game Boy (Color)"), "gb"),
        (_("Game Boy Advance"), "gba"),
        (_("Game Gear"), "gg"),
        (_("Genesis/Mega Drive"), "md"),
        (_("Lynx"), "lynx"),
        (_("Master System"), "sms"),
        (_("Neo Geo Pocket (Color)"), "gnp"),
        (_("NES"), "nes"),
        (_("PC Engine"), "pce_fast"),
        (_("PC-FX"), "pcfx"),
        (_("PlayStation"), "psx"),
        (_("Saturn"), "ss"),
        (_("SNES"), "snes"),
        (_("WonderSwan"), "wswan"),
        (_("Virtual Boy"), "vb"),
    )
    runner_executable = "mednafen/bin/mednafen"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help": _(
                "The game data, commonly called a ROM image. \n" "Mednafen supports GZIP and ZIP compressed ROMs."
            ),
        },
        {
            "option": "machine",
            "type": "choice",
            "label": _("Machine type"),
            "choices": machine_choices,
            "help": _("The emulated machine."),
        },
    ]
    runner_options = [
        {"option": "fs", "type": "bool", "section": _("Graphics"), "label": _("Fullscreen"), "default": False},
        {
            "option": "stretch",
            "type": "choice",
            "section": _("Graphics"),
            "label": _("Aspect ratio"),
            "choices": (
                (_("Disabled"), "0"),
                (_("Stretched"), "full"),
                (_("Preserve aspect ratio"), "aspect"),
                (_("Integer scale"), "aspect_int"),
                (_("Multiple of 2 scale"), "aspect_mult2"),
            ),
            "default": "aspect_int",
        },
        {
            "option": "scaler",
            "type": "choice",
            "section": _("Graphics"),
            "label": _("Video scaler"),
            "choices": (
                ("none", "none"),
                ("hq2x", "hq2x"),
                ("hq3x", "hq3x"),
                ("hq4x", "hq4x"),
                ("scale2x", "scale2x"),
                ("scale3x", "scale3x"),
                ("scale4x", "scale4x"),
                ("2xsai", "2xsai"),
                ("super2xsai", "super2xsai"),
                ("supereagle", "supereagle"),
                ("nn2x", "nn2x"),
                ("nn3x", "nn3x"),
                ("nn4x", "nn4x"),
                ("nny2x", "nny2x"),
                ("nny3x", "nny3x"),
                ("nny4x", "nny4x"),
            ),
            "default": DEFAULT_MEDNAFEN_SCALER,
        },
        {
            "option": "sound_device",
            "type": "choice",
            "label": _("Sound device"),
            "choices": (
                (_("Mednafen default"), "default"),
                (_("ALSA default"), "sexyal-literal-default"),
                ("hw:0", "hw:0,0"),
                ("hw:1", "hw:1,0"),
                ("hw:2", "hw:2,0"),
            ),
            "default": "sexyal-literal-default",
        },
        {
            "option": "dont_map_controllers",
            "type": "bool",
            "label": _("Use default Mednafen controller configuration"),
            "default": False,
        },
    ]

    def get_platform(self):
        machine = self.game_config.get("machine")
        if machine:
            for index, choice in enumerate(self.machine_choices):
                if choice[1] == machine:
                    return self.platforms[index]
        return ""

    def find_joysticks(self):
        """Detect connected joysticks and return their ids"""
        joy_ids = []
        if not self.is_installed:
            return []
        with subprocess.Popen(
            [self.get_executable(), "dummy"],
            stdout=subprocess.PIPE,
            universal_newlines=True,
        ) as mednafen_process:
            output = mednafen_process.communicate()[0].split("\n")
        found = False
        joy_list = []
        for line in output:
            if found and "Joystick" in line:
                joy_list.append(line)
            else:
                found = False
            if "Initializing joysticks" in line:
                found = True

        for joy in joy_list:
            index = joy.find("Unique ID:")
            joy_id = joy[index + 11 :]
            logger.debug("Joystick found id %s ", joy_id)
            joy_ids.append(joy_id)
        return joy_ids

    @staticmethod
    def set_joystick_controls(joy_ids, machine):
        """Setup joystick mappings per machine"""

        # Get the controller mappings
        controller_mappings = get_controller_mappings()
        if not controller_mappings:
            logger.warning("No controller detected for joysticks %s.", joy_ids)
            return []

        # TODO currently only supports the first controller. Add support for other controllers.
        mapping = controller_mappings[0][1]

        # Construct a dictionnary of button codes to parse to mendafen
        map_code = {
            "a": "",
            "b": "",
            "c": "",
            "x": "",
            "y": "",
            "z": "",
            "back": "",
            "start": "",
            "leftshoulder": "",
            "rightshoulder": "",
            "lefttrigger": "",
            "righttrigger": "",
            "leftstick": "",
            "rightstick": "",
            "select": "",
            "shoulder_l": "",
            "shoulder_r": "",
            "i": "",
            "ii": "",
            "iii": "",
            "iv": "",
            "v": "",
            "vi": "",
            "run": "",
            "ls": "",
            "rs": "",
            "fire1": "",
            "fire2": "",
            "option_1": "",
            "option_2": "",
            "cross": "",
            "circle": "",
            "square": "",
            "triangle": "",
            "r1": "",
            "r2": "",
            "l1": "",
            "l2": "",
            "option": "",
            "l": "",
            "r": "",
            "right-x": "",
            "right-y": "",
            "left-x": "",
            "left-y": "",
            "up-x": "",
            "up-y": "",
            "down-x": "",
            "down-y": "",
            "up-l": "",
            "up-r": "",
            "down-l": "",
            "down-r": "",
            "left-l": "",
            "left-r": "",
            "right-l": "",
            "right-r": "",
            "lstick_up": "0000c001",
            "lstick_down": "00008001",
            "lstick_right": "00008000",
            "lstick_left": "0000c000",
            "rstick_up": "0000c003",
            "rstick_down": "00008003",
            "rstick_left": "0000c002",
            "rstick_right": "00008002",
            "dpup": "0000c005",
            "dpdown": "00008005",
            "dpleft": "0000c004",
            "dpright": "00008004",
        }

        # Insert the button mapping number into the map_codes
        for button in mapping.keys:
            bttn_id = mapping.keys[button]
            if bttn_id[0] == "b":  # it's a button
                map_code[button] = "000000" + bttn_id[1:].zfill(2)

        # Duplicate button names that are emulated in mednanfen
        map_code["up"] = map_code["dpup"]  #
        map_code["down"] = map_code["dpdown"]  #
        map_code["left"] = map_code["dpleft"]  # Multiple systems
        map_code["right"] = map_code["dpright"]
        map_code["select"] = map_code["back"]  #
        map_code["shoulder_r"] = map_code["rightshoulder"]  # GBA
        map_code["shoulder_l"] = map_code["leftshoulder"]  #
        map_code["i"] = map_code["b"]  #
        map_code["ii"] = map_code["a"]  #
        map_code["iii"] = map_code["leftshoulder"]
        map_code["iv"] = map_code["y"]  # PCEngine and PCFX
        map_code["v"] = map_code["x"]  #
        map_code["vi"] = map_code["rightshoulder"]
        map_code["run"] = map_code["start"]  #
        map_code["ls"] = map_code["leftshoulder"]  #
        map_code["rs"] = map_code["rightshoulder"]  # Saturn
        map_code["c"] = map_code["righttrigger"]  #
        map_code["z"] = map_code["lefttrigger"]  #
        map_code["fire1"] = map_code["a"]  # Master System
        map_code["fire2"] = map_code["b"]  #
        map_code["option_1"] = map_code["x"]  # Lynx
        map_code["option_2"] = map_code["y"]  #
        map_code["r1"] = map_code["rightshoulder"]  #
        map_code["r2"] = map_code["righttrigger"]  #
        map_code["l1"] = map_code["leftshoulder"]  #
        map_code["l2"] = map_code["lefttrigger"]  # PlayStation
        map_code["cross"] = map_code["a"]  #
        map_code["circle"] = map_code["b"]  #
        map_code["square"] = map_code["x"]  #
        map_code["triangle"] = map_code["y"]  #
        map_code["option"] = map_code["select"]  # NeoGeo pocket
        map_code["l"] = map_code["leftshoulder"]  # SNES
        map_code["r"] = map_code["rightshoulder"]  #
        map_code["right-x"] = map_code["dpright"]  #
        map_code["left-x"] = map_code["dpleft"]  #
        map_code["up-x"] = map_code["dpup"]  #
        map_code["down-x"] = map_code["dpdown"]  # Wonder Swan
        map_code["right-y"] = map_code["lstick_right"]
        map_code["left-y"] = map_code["lstick_left"]  #
        map_code["up-y"] = map_code["lstick_up"]  #
        map_code["down-y"] = map_code["lstick_down"]  #
        map_code["up-l"] = map_code["dpup"]  #
        map_code["down-l"] = map_code["dpdown"]  #
        map_code["left-l"] = map_code["dpleft"]  #
        map_code["right-l"] = map_code["dpright"]  #
        map_code["up-r"] = map_code["rstick_up"]  #
        map_code["down-r"] = map_code["rstick_down"]  # Virtual boy
        map_code["left-r"] = map_code["rstick_left"]  #
        map_code["right-r"] = map_code["rstick_right"]  #
        map_code["lt"] = map_code["leftshoulder"]  #
        map_code["rt"] = map_code["rightshoulder"]  #

        # Define which buttons to use for each machine
        layout = {
            "nes": ["a", "b", "start", "select", "up", "down", "left", "right"],
            "gb": ["a", "b", "start", "select", "up", "down", "left", "right"],
            "gba": [
                "a",
                "b",
                "shoulder_r",
                "shoulder_l",
                "start",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "pce": [
                "i",
                "ii",
                "iii",
                "iv",
                "v",
                "vi",
                "run",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "ss": [
                "a",
                "b",
                "c",
                "x",
                "y",
                "z",
                "ls",
                "rs",
                "start",
                "up",
                "down",
                "left",
                "right",
            ],
            "gg": ["button1", "button2", "start", "up", "down", "left", "right"],
            "md": [
                "a",
                "b",
                "c",
                "x",
                "y",
                "z",
                "start",
                "up",
                "down",
                "left",
                "right",
            ],
            "sms": ["fire1", "fire2", "up", "down", "left", "right"],
            "lynx": ["a", "b", "option_1", "option_2", "up", "down", "left", "right"],
            "psx": [
                "cross",
                "circle",
                "square",
                "triangle",
                "l1",
                "l2",
                "r1",
                "r2",
                "start",
                "select",
                "lstick_up",
                "lstick_down",
                "lstick_right",
                "lstick_left",
                "rstick_up",
                "rstick_down",
                "rstick_left",
                "rstick_right",
                "up",
                "down",
                "left",
                "right",
            ],
            "pcfx": [
                "i",
                "ii",
                "iii",
                "iv",
                "v",
                "vi",
                "run",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "ngp": ["a", "b", "option", "up", "down", "left", "right"],
            "snes": [
                "a",
                "b",
                "x",
                "y",
                "l",
                "r",
                "start",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "wswan": [
                "a",
                "b",
                "right-x",
                "right-y",
                "left-x",
                "left-y",
                "up-x",
                "up-y",
                "down-x",
                "down-y",
                "start",
            ],
            "vb": [
                "up-l",
                "down-l",
                "left-l",
                "right-l",
                "up-r",
                "down-r",
                "left-r",
                "right-r",
                "a",
                "b",
                "lt",
                "rt",
            ],
        }
        # Select a the gamepad type
        controls = []
        if machine in ["gg", "lynx", "wswan", "gb", "gba", "vb"]:
            gamepad = "builtin.gamepad"
        elif machine in ["md"]:
            gamepad = "port1.gamepad6"
            controls.append("-md.input.port1")
            controls.append("gamepad6")
        elif machine in ["psx"]:
            gamepad = "port1.dualshock"
            controls.append("-psx.input.port1")
            controls.append("dualshock")
        else:
            gamepad = "port1.gamepad"

        # Construct the controlls options
        for button in layout[machine]:
            controls.append("-{}.input.{}.{}".format(machine, gamepad, button))
            controls.append("joystick {} {}".format(joy_ids[0], map_code[button]))
        return controls

    def play(self):
        """Runs the game"""
        rom = self.game_config.get("main_file") or ""
        machine = self.game_config.get("machine") or ""

        fullscreen = self.runner_config.get("fs") or "0"
        if fullscreen is True:
            fullscreen = "1"
        elif fullscreen is False:
            fullscreen = "0"

        stretch = self.runner_config.get("stretch") or "0"
        scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
        sound_device = self.runner_config.get("sound_device")

        xres, yres = DISPLAY_MANAGER.get_current_resolution()
        options = [
            "-fs",
            fullscreen,
            "-force_module",
            machine,
            "-sound.device",
            sound_device,
            "-" + machine + ".xres",
            xres,
            "-" + machine + ".yres",
            yres,
            "-" + machine + ".stretch",
            stretch,
            "-" + machine + ".special",
            scaler,
            "-" + machine + ".videoip",
            "1",
        ]
        joy_ids = self.find_joysticks()
        dont_map_controllers = self.runner_config.get("dont_map_controllers")
        if joy_ids and not dont_map_controllers:
            controls = self.set_joystick_controls(joy_ids, machine)
            for control in controls:
                options.append(control)

        if not system.path_exists(rom):
            raise MissingGameExecutableError(filename=rom)

        command = self.get_command()
        for option in options:
            command.append(option)
        command.append(rom)
        return {"command": command}
