#!/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 stat
import subprocess

def which(exe):
    for path in os.environ.get('PATH', '/usr/bin:/bin').split(os.pathsep):
        try:
            abspath = os.path.join(path, exe)
            statbuf = os.stat(abspath)
        except:
            continue
        if stat.S_IMODE(statbuf.st_mode) & 0o111:
            return abspath

    return None

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 Gtk
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('kdialog'):
        subprocess.call(['kdialog', '--error', message, '--title=Doom 2 Master Levels'])
    elif which('xmessage'):
        subprocess.call(['xmessage', '-center', message])
    exit(message)

# 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)"),
}
description = dict()

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

    proc = subprocess.check_output(['update-alternatives', '--list', 'doom'],
                                     universal_newlines=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'):
        if which(alternative):
            alternatives.append(alternative)

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

        self.game = None
        self.warp = None
        self.difficulty = None
        self.engine = None
        self.win = None

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

        if len(alternatives) == 0:
            message = 'No DOOM engine found!\n' + requirements
            def warning_callback(self, data):
                exit(message)
            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(self.win)
            md.connect("response", warning_callback)
            md.show()
        for level in levels.keys():
            level = os.path.splitext(level)[0]
            fullpath = DIR + '/%s.wad' % level
            if not os.path.isfile(fullpath):
                print('\n')
                message = fullpath + " is missing !\n" + requirements
                def warning_callback(self, data):
                    exit(message)
                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(self.win)
                md.connect("response", warning_callback)
                md.show()
            txt = '/usr/share/doc/doom2-masterlevels-wad/%s.txt' % level
            try:
                 with open(txt, 'r', encoding='latin1') as f:
                     description[level] = f.read()
            except (PermissionError, FileNotFoundError):
                description[level] = "failed to read " + txt

        self.setup_ui()

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

        # level list
        games = Gtk.ListStore(str, int)
        for wad in sorted(levels.keys()):
            game = os.path.splitext(wad)[0]
            games.append([game, levels[wad][0] ])

        treeview = Gtk.TreeView(model=games)
        grid.attach(treeview, 0, 0, 1, 8)

        treeviewcolumn = Gtk.TreeViewColumn("Wad")
        treeview.append_column(treeviewcolumn)
        cellrenderertext = Gtk.CellRendererText()
        treeviewcolumn.pack_start(cellrenderertext, True)
        treeviewcolumn.add_attribute(cellrenderertext, "text", 0)

        treeviewcolumn = Gtk.TreeViewColumn("Map")
        treeview.append_column(treeviewcolumn)
        cellrenderertext = Gtk.CellRendererText()
        treeviewcolumn.pack_start(cellrenderertext, True)
        treeviewcolumn.add_attribute(cellrenderertext, "text", 1)

        treeview.connect("query-tooltip", self.tooltip_query)
        treeview.set_tooltip_column(0)
        treeview.connect("cursor-changed", self.select_game)

        # 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)

            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)

            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 tooltip_query(self, treeview, x, y, mode, tooltip):
        path = treeview.get_path_at_pos(x, y - 30) # FIXME
        if path:
            treepath, column = path[:2]
            model = treeview.get_model()
            iter = model.get_iter(treepath)
            game, warp = model[iter]
            wad = game + '.wad'
            if game == 'teeth' and warp == 32: wad += '*'
            tooltip.set_text(levels[wad][1])
        return True

    def select_game(self, treeview):
        self.button_exec.set_sensitive(True)
        (model, pathlist) = treeview.get_selection().get_selected_rows()
        for path in pathlist:
            tree_iter = model.get_iter(path)
            self.game, self.warp = model[tree_iter]
            self.textbuffer.set_text(description[self.game])
            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):
        self.difficulty = int(radio.get_label()[0])

    def select_engine(self, radio):
        self.engine = [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, event):
        subprocess.call(self.engine +
            ['-file', '%s/%s.wad' % (DIR, self.game),
            '-warp', '%d' % self.warp,
            '-skill', '%d' % self.difficulty])

    def chocolate_setup(self, event):
        subprocess.call(['chocolate-doom-setup'])

    def crispy_setup(self, event):
        subprocess.call(['crispy-setup'])

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