#!/usr/bin/python3
# encoding=utf-8
#
# Copyright © 2015-2016 Alexandre Detiste <alexandre@detiste.be>
# Copyright © 2015 Simon McVittie <smcv@debian.org>
# Copyright © 2022 Sébastien Noel <sebastien@twolife.be>
# SPDX-License-Identifier: GPL-2.0-or-later

import os
import subprocess
from shutil import which
from typing import Any


CACODEMON = '/usr/share/pixmaps/doom2-masterlevels.png'

if os.path.isdir('/usr/share/doom'):
    DIR = '/usr/share/doom'
else:
    DIR = '/usr/share/games/doom'

if os.path.isfile('/etc/redhat-release'):
    depedencies = (
        'gtk4, python3-gobject-base and gobject-introspection\n'
        '(already pulled-in by this .rpm)'
    )
    command = 'dnf install prboom-plus'
else:
    depedencies = 'python3-gi and gir1.2-gtk-4.0'
    command = 'apt-get install doom-engine python3-gi gir1.2-gtk-4.0'

requirements = '''
--------------------------------------------------------

You need those to make use this launcher:
* the .wad files from DOOM 2 Master Levels
* some Doom game engine
* ''' + depedencies + '''

The free parts can be obtained this way:
  ''' + command + '''

The .wad files can be for example bought on Steam:
https://store.steampowered.com/app/9160/ or found
on "Doom 3: Resurrection of Evil" Xbox game disc.

The data then need to be put at the right location.
You can use game-data-packager(6) to automate this.

It will also automatically pick up the data downloaded
by a windows Steam instance running through Wine.
'''

# ValueError: Namespace Gtk not available
try:
    import gi
    gi.require_version('Gtk', '4.0')
    from gi.repository import GObject, Gio, Gtk, Gdk
except (ImportError, ValueError):
    message = 'Python3 Gtk+ libraries not found!\n' + requirements
    if which('zenity'):
        subprocess.call([
            'zenity', '--error', '--title=Doom 2 Master Levels', '--text',
            message,
        ])
    elif which('yad'):
        subprocess.call([
            'yad', '--error', '--title=Doom 2 Master Levels', '--text',
            message,
        ])
    elif which('kdialog'):
        subprocess.call([
            'kdialog', '--error', message, '--title=Doom 2 Master Levels',
        ])
    elif which('xmessage'):
        subprocess.call(['xmessage', '-center', message])
    exit(message)


setup_tools = {
    'chocolate-doom': 'chocolate-doom-setup',
    'crispy-doom': 'crispy-setup',
    'woof': 'woof-setup',
}

# wad : (warp, longname,  'https://doomwiki.org/wiki/' + url)
levels = {
    'attack.wad': (
        1, 'Attack', 'MAP01:_Attack_(Master_Levels)',
    ),
    'blacktwr.wad': (
        25, 'Black Tower', 'MAP25:_Black_Tower_(Master_Levels)',
    ),
    'bloodsea.wad': (
        7, 'Bloodsea Keep', 'MAP07:_Bloodsea_Keep_(Master_Levels)',
    ),
    'canyon.wad': (
        1, 'Canyon', 'MAP01:_Canyon_(Master_Levels)',
    ),
    'catwalk.wad': (
        1, 'The Catwalk', 'MAP01:_The_Catwalk_(Master_Levels)',
    ),
    'combine.wad': (
        1, 'The Combine', 'MAP01:_The_Combine_(Master_Levels)',
    ),
    'fistula.wad': (
        1, 'The Fistula', 'MAP01:_The_Fistula_(Master_Levels)',
    ),
    'garrison.wad': (
        1, 'The Garrison', 'MAP01:_The_Garrison_(Master_Levels)',
    ),
    'geryon.wad': (
        8, 'Geryon: 6th Canto of Inferno', 'MAP08:_Geryon_(Master_Levels)',
    ),
    'mephisto.wad': (
        7,
        "Mephisto's Maosoleum",
        "MAP07:_Mephisto%27s_Maosoleum_(Master_Levels)",
    ),
    'manor.wad': (
        1, 'Titan Manor', 'MAP01:_Titan_Manor_(Master_Levels)',
    ),
    'minos.wad': (
        5,
        "Minos' Judgement: 4th Canto of Inferno",
        "MAP05:_Minos%27_Judgement_(Master_Levels)",
    ),
    'nessus.wad': (
        7, 'Nessus: 5th Canto of Inferno', "MAP07:_Nessus_(Master_Levels)",
    ),
    'paradox.wad': (
        1, 'Paradox', 'MAP01:_Paradox_(Master_Levels)',
    ),
    'subspace.wad': (
        1, 'Subspace', 'MAP01:_Subspace_(Master_Levels)',
    ),
    'subterra.wad': (
        1, 'Subterra', 'MAP01:_Subterra_(Master_Levels)',
    ),
    'teeth.wad': (
        31,
        'The Express Elevator to Hell',
        'MAP31:_The_Express_Elevator_to_Hell_-_teeth.wad_(Master_Levels)',
    ),
    'teeth.wad*': (
        32,
        'Bad Dream',
        'MAP32:_Bad_Dream_-_teeth.wad_(Master_Levels)',
    ),
    'ttrap.wad': (
        1, 'Trapped on Titan', 'MAP01:_Trapped_on_Titan_(Master_Levels)',
    ),
    'vesperas.wad': (
        9, 'Vesperas: 7th Canto of Inferno', 'MAP09:_Vesperas_(Master_Levels)',
    ),
    'virgil.wad': (
        3,
        "Virgil's Lead: 3rd Canto of Inferno",
        "MAP03:_Virgil%27s_Lead_(Master_Levels)",
    ),
}

alternatives: list[str] = []
if os.path.islink('/etc/alternatives/doom'):
    # on Debian
    alternatives = [os.readlink('/etc/alternatives/doom')]

    proc = subprocess.check_output(
        ['update-alternatives', '--list', 'doom'],
        text=True,
    )
    for alternative in proc.splitlines():
        if alternative not in alternatives:
            alternatives.append(alternative)
else:
    # not on Debian
    for alternative in ('prboom-plus', 'prboom', 'chocolate-doom',
                        'crispy-doom', 'woof', 'dsda-doom'):
        if which(alternative):
            alternatives.append(alternative)


class MasterLevel(GObject.GObject):
    def __init__(
        self,
        game: str,
        warp: int,
        longname: str,
        url: str,
        description: str,
    ):
        super().__init__()
        self.game = game
        self.warp = warp
        self.longname = longname
        self.url = url
        self.description = description


class Launcher(Gtk.Application):
    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.connect('activate', self.on_activate)

        self.game: str = ''
        self.warp: int = 0
        self.difficulty: int = 3
        self.engine: list[str] = []
        self.win: Gtk.ApplicationWindow
        self.games: Gio.ListStore

    def on_activate(self, app: 'Launcher') -> None:
        self.win = Gtk.ApplicationWindow(application=self)
        self.win.set_title("Doom 2 Master Levels")
        self.win.set_default_size(1020, 650)
        self.win.present()

        def warning_callback(self: Gtk.MessageDialog, data: int) -> None:
            exit(message)

        if len(alternatives) == 0:
            message = 'No DOOM engine found!\n' + requirements
            md = Gtk.MessageDialog(title='Warning',
                                   message_type=Gtk.MessageType.WARNING,
                                   buttons=Gtk.ButtonsType.CLOSE,
                                   text=message)
            md.set_transient_for(self.win)
            md.set_modal(True)
            md.connect("response", warning_callback)
            md.set_visible(True)

        self.games = Gio.ListStore.new(MasterLevel)
        for k in sorted(levels.keys()):
            warp = levels[k][0]
            longname = levels[k][1]
            url = levels[k][2]
            level = os.path.splitext(k)[0]
            fullpath = DIR + '/%s.wad' % level
            if not os.path.isfile(fullpath):
                print('\n')
                message = fullpath + " is missing !\n" + requirements
                md = Gtk.MessageDialog(title='Warning',
                                       message_type=Gtk.MessageType.WARNING,
                                       buttons=Gtk.ButtonsType.CLOSE,
                                       text=message)
                md.set_transient_for(self.win)
                md.set_modal(True)
                md.connect("response", warning_callback)
                md.set_visible(True)
            txt = '/usr/share/doc/doom2-masterlevels-wad/%s.txt' % level
            try:
                with open(txt, 'r', encoding='latin1') as f:
                    description = f.read()
            except (PermissionError, FileNotFoundError):
                description = "failed to read " + txt

            self.games.append(
                MasterLevel(level, warp, longname, url, description),
            )

        self.setup_ui()

    def setup_ui(self) -> None:
        grid = Gtk.Grid()
        grid.set_row_spacing(5)
        grid.set_column_spacing(5)
        self.win.set_child(grid)

        css_provider = Gtk.CssProvider()
        if Gtk.get_minor_version() < 12:
            css_provider.load_from_data(
                "label.smaller_row { margin-top: -5pt; margin-bottom: -2pt; }",
                -1,
            )
        else:
            css_provider.load_from_string(
                "label.smaller_row { margin-top: -5pt; margin-bottom: -2pt; }",
            )
        default_display = Gdk.Display.get_default()
        assert default_display is not None
        Gtk.StyleContext.add_provider_for_display(
            default_display, css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
        )

        # level list
        def f_bind_setup(
            fact: Gtk.SignalListItemFactory,
            item: Gtk.ListItem,
        ) -> None:
            label = Gtk.Inscription()
            label.add_css_class("smaller_row")
            item.set_child(label)

        def f_bind_col1(
            fact: Gtk.SignalListItemFactory,
            item: Gtk.ListItem,
        ) -> None:
            level = item.get_item()
            assert isinstance(level, MasterLevel)
            label = item.get_child()
            assert isinstance(label, Gtk.Inscription)
            label.set_text(level.game)
            label.set_tooltip_text(level.longname)

        def f_bind_col2(
            fact: Gtk.SignalListItemFactory,
            item: Gtk.ListItem,
        ) -> None:
            level = item.get_item()
            assert isinstance(level, MasterLevel)
            label = item.get_child()
            assert isinstance(label, Gtk.Inscription)
            label.set_text(str(level.warp))
            label.set_tooltip_text(level.longname)

        selection = Gtk.SingleSelection.new(self.games)
        selection.connect("selection-changed", self.select_game)
        columnview = Gtk.ColumnView.new(selection)
        col1factory = Gtk.SignalListItemFactory()
        col2factory = Gtk.SignalListItemFactory()
        col1factory.connect("setup", f_bind_setup)
        col2factory.connect("setup", f_bind_setup)
        col1factory.connect("bind", f_bind_col1)
        col2factory.connect("bind", f_bind_col2)
        col1 = Gtk.ColumnViewColumn.new("Wad", col1factory)
        col1.set_fixed_width(80)
        col2 = Gtk.ColumnViewColumn.new("Map", col2factory)
        columnview.append_column(col1)
        columnview.append_column(col2)
        grid.attach(columnview, 0, 0, 1, 8)

        # header
        label = Gtk.Label()
        label.set_markup("<span size='xx-large'>Doom II Master Levels</span>")
        grid.attach(label, 1, 0, 1, 1)

        logo = Gtk.Image()
        logo.set_from_file(CACODEMON)
        logo.set_pixel_size(72)
        grid.attach(logo, 2, 0, 1, 1)

        # description
        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.set_policy(
            Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC,
        )
        grid.attach(scrolledwindow, 1, 1, 2, 1)

        self.textbuffer = Gtk.TextBuffer()
        self.textbuffer.set_text(
            'Please select a map from the list on the left',
        )

        textview = Gtk.TextView(buffer=self.textbuffer)
        textview.set_vexpand(True)
        textview.set_hexpand(True)
        textview.set_property('editable', False)
        textview.set_monospace(True)
        scrolledwindow.set_child(textview)

        self.doomwiki = Gtk.LinkButton(
            uri="https://doomwiki.org/wiki/Master_Levels_for_Doom_II",
            label="https://doomwiki.org/wiki/Master_Levels_for_Doom_II",
        )
        grid.attach(self.doomwiki, 1, 2, 2, 1)

        # difficulty
        difflabel = Gtk.Label(label="Choose your difficulty")
        grid.attach(difflabel, 1, 3, 1, 1)

        diffgrid = Gtk.Grid()
        diffradio = Gtk.CheckButton(label="1)  I'm too young to die")
        diffgrid.attach(diffradio, 0, 0, 1, 1)
        diffradio.connect('toggled', self.select_difficulty)
        for diff in ["2)  Hey, Not too Rough",
                     "3)  Hurt me plenty",
                     "4)  Ultra Violence",
                     "5)  Nightmare!"]:
            radiobutton = Gtk.CheckButton(label=diff)
            radiobutton.set_group(diffradio)
            radiobutton.connect('toggled', self.select_difficulty)
            if diff[0] == '3':
                radiobutton.set_active(True)
            diffgrid.attach(radiobutton, 0, int(diff[0]), 1, 1)
        grid.attach(diffgrid, 1, 4, 1, 1)

        # engine
        label = Gtk.Label(label="Choose your engine")
        grid.attach(label, 2, 3, 1, 1)
        radiogrid = Gtk.Grid()
        radiobuttonDefault = Gtk.CheckButton(label="n/a")
        radiobuttonDefault.set_visible(False)

        i = 0
        for alternative in alternatives:
            if alternative == '/usr/games/doomsday-compat':
                alternative = '/usr/games/doomsday'

            radiobutton = Gtk.CheckButton(label=alternative)
            radiobutton.set_group(radiobuttonDefault)
            radiobutton.connect('toggled', self.select_engine)
            radiogrid.attach(radiobutton, 0, i, 1, 1)

            if i == 0:
                radiobutton.set_label("%s (default)" % alternative)
                radiobutton.set_active(True)
                self.select_engine(radiobutton)

            # TODO: factorize these 3 stanzas & the 3 methods
            if (
                os.path.basename(alternative) == 'chocolate-doom'
                and which('chocolate-doom-setup')
            ):
                self.button_conf = Gtk.Button(label="Configure")
                radiogrid.attach(self.button_conf, 1, i, 1, 1)
                self.button_conf.connect("clicked", self.chocolate_setup)

            if (
                os.path.basename(alternative) == 'crispy-doom'
                and which('crispy-setup')
            ):
                self.button_conf = Gtk.Button(label="Configure")
                radiogrid.attach(self.button_conf, 1, i, 1, 1)
                self.button_conf.connect("clicked", self.crispy_setup)

            if (
                os.path.basename(alternative) == 'woof'
                and which('woof-setup')
            ):
                self.button_conf = Gtk.Button(label="Configure")
                radiogrid.attach(self.button_conf, 1, i, 1, 1)
                self.button_conf.connect("clicked", self.woof_setup)

            i += 1

        if i > 1 and os.path.isfile('/etc/debian_version'):
            radiogrid.set_tooltip_text(
                'Default can be changed with '
                '"update-alternatives --config doom"'
            )

        grid.attach(radiogrid, 2, 4, 1, 1)

        # Run !
        self.button_exec = Gtk.Button(label="Run")
        self.button_exec.set_sensitive(False)
        grid.attach(self.button_exec, 1, 6, 1, 1)
        self.button_exec.connect("clicked", self.run_game)

        button_quit = Gtk.Button(label="Exit")
        grid.attach(button_quit, 2, 6, 1, 1)
        button_quit.connect("clicked", lambda _: self.win.close())

    def select_game(
        self,
        selection: Gtk.SingleSelection,
        position: int,
        n_items: int,
    ) -> None:
        selected_item = selection.get_selected_item()
        assert isinstance(selected_item, MasterLevel)
        if selected_item is not None:
            self.button_exec.set_sensitive(True)
            self.game = selected_item.game
            self.warp = selected_item.warp
            self.textbuffer.set_text(selected_item.description)
            wad = self.game + '.wad'
            if self.game == 'teeth' and self.warp == 32:
                wad += '*'
            url = 'https://doomwiki.org/wiki/' + levels[wad][2]
            self.doomwiki.set_uri(url)
            self.doomwiki.set_label(url)

    def select_difficulty(self, radio: Gtk.CheckButton) -> None:
        self.difficulty = int(str(radio.get_label())[0])

    def select_engine(self, radio: Gtk.CheckButton) -> None:
        self.engine = [str(radio.get_label()).split(' ')[0]]
        if self.engine == ['/usr/games/doomsday']:
            self.engine.append('-game')
            self.engine.append('doom2')
        if self.engine == ['chocolate-doom'] and DIR == '/usr/share/doom':
            self.engine.append('-iwad')
            self.engine.append('/usr/share/doom/doom2.wad')

    def run_game(self, button: Gtk.Button) -> None:
        subprocess.call(
            self.engine + [
                '-file', '%s/%s.wad' % (DIR, self.game),
                '-warp', '%d' % self.warp,
                '-skill', '%d' % self.difficulty,
            ],
        )

    def chocolate_setup(self, button: Gtk.Button) -> None:
        subprocess.call(['chocolate-doom-setup'])

    def crispy_setup(self, button: Gtk.Button) -> None:
        subprocess.call(['crispy-setup'])

    def woof_setup(self, button: Gtk.Button) -> None:
        subprocess.call(['woof-setup'])


if __name__ == "__main__":
    launcher = Launcher(
        application_id="net.debian.game_data_packager.doom2_masterlevels",
    )
    launcher.run(None)
