#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# py-indent-offset:4 -*-

#-------------------------------------------------------------------------------
# Name:             quiz.py
# Purpose:          Country flags quiz game
# Author:           Francesc Zacarias
# Created:          2018
# Copyright:        Francesc Zacarias
# License:          GNU General Public License v3
# Version:          2.0.0
# Updates:          30/04/2025 (d/m/y) 
# ....              Updates and improvements by Ecco
# Tags:             phoenix-port, py3-port
# ....
# Tested:           - Windows 10/11 | Python 3.11.9 |
# ....              wxPython 4.2.3 | wxWidgets 3.2.6
# ....
# ....              - Linux Mint 21 | Python 3.10.12 |
# ....              wxPython 4.2.1 gtk3 | wxWidgets 3.2.2.1
# ....
# ....              - MacOS Sequoia 15 | Python 3.12.4 |
# ....              wxPython 4.2.2 | wxWidgets 3.2.6
# ....
# Thanks to:        ...
#-------------------------------------------------------------------------------

"""

A question/answer game where you have to find
the names of the countries corresponding to the
flags displayed.

"""

#-------------------------------------------------------------------------------

# 2.0.0 - Ecco :
#       - adding of a menu bar with items
#       - saving the latest result inside a text file
#       - adding of the number of questions remaining to the panel
#       - possibility to configure the number of questions
#       - deleting flickers (for Windows)
#       - general code improvement
#       - visual changes (button, text control, background...)
#       - adding confirmation windows
#       - adding new country flags (from Countryflags.com)
#         Free images for private and educational use only (Non-commercial use).
# 1.0.0 - First release (Francesc Zacarias)

#-------------------------------------------------------------------------------
# Import python packages
#-------------------------------------------------------------------------------
import sys
import os.path
import random
import json
from   argparse import ArgumentParser

#-------------------------------------------------------------------------------
# Import wxPython packages
#-------------------------------------------------------------------------------
import wx
import wx.lib.colourdb
import embedded_images
import wx.lib.mixins.listctrl as listmix
from   gradient_button import GradientButton
from   textctrl import TextCtrlPanel
from   statbmp  import GenStaticBitmap as StaticBitmap  # No flickers

#-------------------------------------------------------------------------------
# Constant
#-------------------------------------------------------------------------------
TITLE       = "Country flags quiz"
VERSION     = "2.0.0"

#-------------------------------------------------------------------------------
# Classes
#-------------------------------------------------------------------------------
# class Scores
# class InvalidConfig
# class Utils
# class BufferedPanel
# class ListCtrl
# class ConfirmationDialog
# class ScoreDialog
# class Game
# class QuestionFrame

#-------------------------------------------------------------------------------

class Scores(object):
    @classmethod
    def get_success(cls, scores):
        return Utils.get_success(scores)

    @classmethod
    def get_grade(cls, rate):
        return Utils.get_grade(rate)

#-------------------------------------------------------------------------------
    
class InvalidConfig(Exception):
    def __str__(self):
        return "Invalid configuration: {}".format(self.args[0])

#-------------------------------------------------------------------------------
    
class Utils(object):
    @staticmethod
    def load_config(cfgpath):
        with open(cfgpath, 'r', encoding='utf-8') as f:
            return json.load(f)

    @staticmethod
    def validate_config(cfg):
        required_keys = ['title', 'questions']
        for key in required_keys:
            if key not in cfg:
                raise InvalidConfig(f"Missing '{key}' key in config.")

        for q in cfg['questions']:
            if not isinstance(q, list) or len(q) != 2:
                raise InvalidConfig("Wrong format in question {}".format(q))
            img_path, answer = q
            if not isinstance(img_path, str) or not isinstance(answer, str):
                raise InvalidConfig("Wrong types in question {}".format(q))
            if not os.path.exists(img_path):
                raise InvalidConfig("Cannot find file {}".format(img_path))
        return True

    @staticmethod
    def load_images(questions):
        """
        Return questions list with wx.Bitmap objects instead of file paths.
        """
        img_questions = []
        for imgpath, answer in questions:
            try:
                img = wx.Image(imgpath, wx.BITMAP_TYPE_ANY).ConvertToBitmap()
                img_questions.append((img, answer))
            except Exception as e:
                # Log error and skip image
                print(f"Error loading image {imgpath}: {e}")
        return img_questions

    @staticmethod
    def get_success(scores):
        if not scores:
            return 0.0
        num_success = len([x for x in scores if x[1]])
        return (num_success / len(scores)) * 100

    @staticmethod
    def get_grade(rate):
        grades = [
            (100.0, "Perfect! You did it!"),
            (99.0, "Almost perfect! Keep trying!"),
            (90.0, "Alright !"),
            (75.0, "GOOD !"),
            (50.0, "You are succeeding, but there is room for improvement."),
            (0.0, "\nYou failed!\nYou need to practice more."),
        ]
        for max_grade, response in grades:
            if rate >= max_grade:
                return response
        raise ValueError('get_grade: Rate must be a positive number')

#-------------------------------------------------------------------------------
    
class BufferedPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(parent, style=wx.TAB_TRAVERSAL)
        
        self.SetBackgroundColour(wx.Colour(20, 20, 20))
        
        if wx.Platform == "__WXMSW__":
            if not self.IsDoubleBuffered():
                self.SetDoubleBuffered(True)

#-------------------------------------------------------------------------------
                
class ListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
    def __init__(self, parent, id, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=0):
        wx.ListCtrl.__init__(self, parent, id, pos, size, style)
        listmix.ListCtrlAutoWidthMixin.__init__(self)

#-------------------------------------------------------------------------------
        
class ConfirmationDialog(wx.Dialog):
    def __init__(self, parent, message, title):
        super().__init__(parent, title=title)

        # Colourdb
        wx.lib.colourdb.updateColourDB()
        
        self.result = None
        self.ft_size = self.get_font_size()
        
        self.SetSize((-1, 180))
        self.SetSizeHints(self.GetSize(), self.GetSize())
        self.SetMaxSize(self.GetSize())
        self.SetMinSize(self.GetSize())

        panel = BufferedPanel(self)
        panel.SetForegroundColour("#ffffff") 

        msg_text = wx.StaticText(panel, label=message)

        self.line = wx.StaticLine(panel, style=wx.LI_HORIZONTAL)
        self.line.SetBackgroundColour(wx.WHITE)  # For Linux

        self.new_btn = GradientButton(panel, id=wx.ID_NO, label="New game",
                       normalLabelColour="#ffffff", hoverLabelColour="#ffffff",
                       pressedLabelColour="#ffffff", font_size=self.ft_size,
                       font_weight=wx.FONTWEIGHT_BOLD,
                       normalBtnColour=(wx.Colour(146, 193, 48, 255), wx.Colour(75, 120, 21, 255)),
                       hoverBtnColour="#92c130", pressedBtnColour="#4b7815",
                       disabledBtnColour="#838383", focusColour="#ffffff",
                       focusSize=3,
                       borderRadius=4, size=(150, 30))
        
        self.quit_btn = GradientButton(panel, id=wx.ID_YES, label="Quit",
                        normalLabelColour="#ffffff", hoverLabelColour="#ffffff",
                        pressedLabelColour="#ffffff", font_size=self.ft_size,
                        font_weight=wx.FONTWEIGHT_BOLD,
                        normalBtnColour=(wx.Colour(146, 193, 48, 255), wx.Colour(75, 120, 21, 255)),
                        hoverBtnColour="#92c130", pressedBtnColour="#4b7815",
                        disabledBtnColour="#838383", focusColour="#ffffff",
                        focusSize=3,
                        borderRadius=4, size=(100, 30))
        self.quit_btn.SetFocus()
        
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.AddStretchSpacer()
        main_sizer.Add(msg_text, 0, wx.ALL | wx.CENTER, 10)
        main_sizer.AddStretchSpacer()
        main_sizer.Add(self.line, 0, wx.EXPAND | wx.ALL, 10)

        btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
        btn_sizer.Add(self.new_btn, 0, wx.ALL, 10)
        btn_sizer.Add(self.quit_btn, 0, wx.ALL, 10)

        btn_container = wx.BoxSizer(wx.HORIZONTAL)
        btn_container.Add(btn_sizer)

        main_sizer.Add(btn_container, 0, wx.ALIGN_CENTER | wx.BOTTOM, 5)
        panel.SetSizer(main_sizer)
        self.Fit()

        self.new_btn.Bind(wx.EVT_BUTTON, self.on_new_game)
        self.quit_btn.Bind(wx.EVT_BUTTON, self.on_quit)

    def get_font_size(self):
        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        font_size = font.GetPointSize()

        # Dictionary for size adjustments by platform  
        platform_adjustments = {
            '__WXMSW__': 1,
            '__WXGTK__': -1,
            '__WXMAC__': 2
        }
        # Use get() to get the fit, or 0 if the platform is not specified
        adjustment = platform_adjustments.get(wx.Platform, 0)
        return font_size + adjustment  # Returns the adjusted font size

    def on_new_game(self, event):
        self.result = wx.ID_NO
        self.Close()

    def on_quit(self, event):
        self.result = wx.ID_YES
        self.Close()

#-------------------------------------------------------------------------------
        
class ScoreDialog(wx.Dialog):
    def __init__(self, parent, scores, title="Scores"):
        super().__init__(parent, title=title, style=wx.DEFAULT_DIALOG_STYLE)

        # Colourdb
        wx.lib.colourdb.updateColourDB()
        
        self.SetSize((380, 500))
        self.SetSizeHints((380, 500))
        self.SetDoubleBuffered(True)
        self.SetBackgroundColour(wx.Colour(20, 20, 20))
        self.ft_size = self.get_font_size()
        
        panel = wx.Panel(self, -1)

        self.scores = scores        
        success_rate = Utils.get_success(self.scores)
        grade = Utils.get_grade(success_rate)

        txt = wx.StaticText(panel,
            label="Score : {:.2f}%\n{}".format(success_rate, grade)
        )
        txt.SetForegroundColour("#ffffff")
        txt.SetBackgroundColour(wx.Colour(20, 20, 20))

        self.lst = ListCtrl(panel, -1,
                       style=wx.LC_REPORT | wx.LC_SINGLE_SEL |
                             wx.LC_VRULES | wx.LC_HRULES)
        # Insert columns with fixed width or 0, then adjust (for Linux)
        self.lst.InsertColumn(0, "#", width=40)
        self.lst.InsertColumn(1, "Question", width=200)
        self.lst.InsertColumn(2, "Correct", width=60)
        self.lst.SetForegroundColour(wx.Colour(0, 0, 0))
        self.lst.SetBackgroundColour(wx.Colour(20, 20, 20))

        # Attributes (textcolor, backgroundcolor, font)
        listfont = self.lst.GetFont()
        headfont = listfont.MakeBold()
        headAttr = wx.ItemAttr((0,0,0), (240,240,240), headfont)

        # NOTE : Currently it is implemented only for wxMSW and
        # does nothing in the other ports   
        self.lst.SetHeaderAttr(headAttr)
        
        for count, (answer, correct) in enumerate(self.scores, start=1):
            self.lst.Append((count, answer.title(), 'YES' if correct else 'NO'))

        self.lst.SetColumnWidth(0, wx.LIST_AUTOSIZE_USEHEADER)
        self.lst.SetColumnWidth(1, wx.LIST_AUTOSIZE_USEHEADER)
        self.lst.SetColumnWidth(2, wx.LIST_AUTOSIZE_USEHEADER)
        self.lst.SetBackgroundColour("#eceade")

        # OK button
        ok_btn = GradientButton(
            panel, id=wx.ID_CLOSE, label="OK",
            normalLabelColour="#ffffff", hoverLabelColour="#ffffff",
            pressedLabelColour="#ffffff", font_size=self.ft_size,
            font_weight=wx.FONTWEIGHT_BOLD,
            normalBtnColour=(wx.Colour(146, 193, 48, 255), wx.Colour(75, 120, 21, 255)),
            hoverBtnColour="#92c130", pressedBtnColour="#4b7815",
            disabledBtnColour="#838383", focusColour="#ffffff",
            indicatorColour="#ffffff", focusSize=3,
            borderRadius=4, size=(100, 30)
        )
        ok_btn.SetFocus()
        ok_btn.Bind(wx.EVT_BUTTON, self.on_close_window, id=wx.ID_CLOSE)

        self.Bind(wx.EVT_CLOSE, self.on_close_window)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(txt, 0, wx.ALIGN_CENTER | wx.ALL, 10)
        sizer.Add(self.lst, 1, wx.EXPAND | wx.ALL, 10)
        sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)

        panel.SetSizer(sizer)

    def get_font_size(self):
        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
        font_size = font.GetPointSize()

        # Dictionary for size adjustments by platform  
        platform_adjustments = {
            '__WXMSW__': 1,
            '__WXGTK__': -1,
            '__WXMAC__': 2
        }
        # Use get() to get the fit, or 0 if the platform is not specified
        adjustment = platform_adjustments.get(wx.Platform, 0)
        return font_size + adjustment  # Returns the adjusted font size

    def save_scores_to_file(self, filename="scores.txt"):
        try:
            with open(filename, 'w', encoding='utf-8') as f:
                header = "ID".ljust(5) + "Question".ljust(30) + "Correct".ljust(10)
                f.write(header + "\n")
                for item_idx in range(self.lst.GetItemCount()):
                    item_id = self.lst.GetItemText(item_idx, 0)
                    question = self.lst.GetItemText(item_idx, 1)
                    correct = self.lst.GetItemText(item_idx, 2)
                    line = (
                        str(item_id).ljust(5) +
                        question.ljust(30) +
                        correct.ljust(10)
                    )
                    f.write(line + "\n")
            wx.MessageBox(f"The scores were saved in the file \"{filename}\" (game directory).", "Information", wx.OK | wx.ICON_INFORMATION)
        except Exception as e:
            wx.MessageBox(f"Error saving: {e}", "Error", wx.OK | wx.ICON_ERROR)

    def on_close_window(self, event):
        self.EndModal(0)

#-------------------------------------------------------------------------------
        
class Game(object):
    def __init__(self, number_questions, cfg):
        """
        Initializes the game with a specified number of questions and configuration.
        """

        self.cfg_path = cfg['cfg_path']  # Loading configuration path
        self.number_questions = number_questions  # Number of questions to ask
        self.initial_number_questions = number_questions  # Saving the initial number
        self.title = cfg['title']  # Game title
        self.scores = []  # List to store scores

        # Loading questions and images
        self.questions_data = cfg['questions']
        self.images = Utils.load_images(self.questions_data)

        total_questions = len(self.images)
        if total_questions == 0:
            raise RuntimeError("No questions loaded, check the configuration.")

        # Question selection
        if number_questions == 0:
            self.questions = self.images.copy()
        else:
            self.questions = random.sample(self.images, min(number_questions, total_questions))
        self.current = 0

        # Creating the question window
        self.frame = QuestionFrame(
            None,
            game=self,
            scores=self.scores,
            title=f"{self.title} v{VERSION}",
            cfg_path=self.cfg_path
        )
        self.frame.update_remaining_questions()  
        self.next_image()

    def set_number_questions(self, new_num):
        """
        Updates the number of questions to ask and randomly reselects if necessary.
        """
        total_questions = len(self.images)
        if new_num == 0:
            self.questions = self.images.copy()
        else:
            self.questions = random.sample(self.images, min(new_num, total_questions))
        self.number_questions = new_num
        self.current = 0

    def next_image(self):
        """
        Shows the next question or ends the game if all questions have been answered.
        """
        if self.current < len(self.questions):
            img, answer = self.questions[self.current]
            self.frame.set_image(img, answer)
            self.current += 1
            self.frame.update_remaining_questions()   
        else:
            self.frame.end_remaining_questions()
            # End of quiz
            # Before closing the window, save the scores
            endFrame = ScoreDialog(
                None,
                scores=self.scores,
                title="Scores"
            )
            # Display the modal dialog
            val = endFrame.ShowModal()

            # Save the scores before showing the window
            endFrame.save_scores_to_file()
            endFrame.Destroy()  # Next, close the window
            self.frame.Close()  # Close the main window after saving

    def reset(self):
        # Uses the initial value
        self.set_number_questions(self.initial_number_questions)
        self.scores.clear()
        self.current = -1
        self.next_image()

#-------------------------------------------------------------------------------
        
class QuestionFrame(wx.Frame):
    def __init__(self, *args, **kw):
        self.game = kw.pop('game')
        self.scores = kw.pop('scores')
        self.cfg_path = kw.pop('cfg_path')
        style = wx.MINIMIZE_BOX | wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX | wx.CLIP_CHILDREN
        super().__init__(*args, style=style, **kw)

        # Removes the white background from the main window
        # when the game is closed (Windows only)
        if wx.Platform == "__WXMSW__":
            if not self.IsDoubleBuffered():
                self.SetDoubleBuffered(True) 

        width = 780
        height = 540

        self.set_properties()
        self.make_menu_bar()
        self.make_status_bar()

        # Main panel
        self.panel = BufferedPanel(self)
        self.SetBackgroundColour(wx.Colour(20, 20, 20))

        # Panel for remaining questions label
        self.remaining_questions_pnl = wx.Panel(self.panel)
        self.remaining_questions_sizer = wx.BoxSizer(wx.VERTICAL)
        self.remaining_questions_label = wx.StaticText(self.remaining_questions_pnl, label="")
        self.remaining_questions_label.SetForegroundColour(wx.WHITE)
        self.remaining_questions_sizer.Add(self.remaining_questions_label, 0, wx.ALL | wx.ALIGN_CENTER, 5)
        self.remaining_questions_pnl.SetSizer(self.remaining_questions_sizer)
        self.remaining_questions_pnl.SetBackgroundColour(wx.Colour(50, 50, 50)) 
        
        # Create the StaticBitmap with empty bitmap
        # GenStaticBitmap is better than wx.StaticBitmap
        empty_bitmap = wx.Bitmap(1, 1)
        self.bitmap = StaticBitmap(self.panel, -1, empty_bitmap)   # No flickers
        self.txt = TextCtrlPanel(
            self.panel,
            id=-1,
            brushColour=(wx.Colour(132, 178, 43, 255)),
            penColour=(wx.Colour(30, 30, 30, 130)),
            penThickness=1,
            rounded=4,
            shadowColour=(wx.Colour(255, 255, 255, 255)),
            textColour=wx.Colour(0, 0, 0, 255)
        )
        self.txt.SetValue("")
        self.txt.SetFocus()

        # Layout principal
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.remaining_questions_pnl, proportion=0, flag=wx.EXPAND | wx.ALL, border=10)
        self.sizer.AddStretchSpacer()
        h_sizer = wx.BoxSizer(wx.HORIZONTAL)
        h_sizer.AddStretchSpacer()
        h_sizer.Add(self.bitmap, 0, wx.ALIGN_CENTER)
        self.sizer.Add(h_sizer, 0, wx.ALIGN_CENTER)
        self.sizer.AddStretchSpacer()
        self.sizer.Add(self.txt, proportion=0, flag=wx.EXPAND | wx.ALL, border=10)

        self.panel.SetSizer(self.sizer)

        # Bindings
        self.panel.Bind(wx.EVT_SIZE, self.on_resize)
        self.txt.Bind(wx.EVT_TEXT_ENTER, self.validate_answer)
        self.Bind(wx.EVT_WINDOW_MODAL_DIALOG_CLOSED, self.on_window_modal_dialog_closed)
        self.Bind(wx.EVT_CLOSE, self.on_close_window)

        # Center the bitmap
        self.center_bitmap()

        # Initialize the text of the remaining questions panel
        self.update_remaining_questions()

        self.SetClientSize((width, height))
        self.Fit()
        self.Centre()
        self.Show()
        
    def set_properties(self):
        self.SetTitle(f"Country flags quiz v{VERSION}")
        bmp = embedded_images.icon.GetBitmap()
        if bmp.GetSize() != (32, 32):
            img = bmp.ConvertToImage()
            img = img.Rescale(32, 32, wx.IMAGE_QUALITY_HIGH)
            bmp = wx.Bitmap(img)
        icon = wx.Icon()
        icon.CopyFromBitmap(bmp)
        self.SetIcon(icon)
        
    def make_menu_bar(self):        
        # Menu bar
        menu_bar = wx.MenuBar()

        file_menu = wx.Menu()

        self.menu_item_news = wx.MenuItem(file_menu, wx.ID_ANY, "&New game\tCtrl+N", "Start a new game")
        file_menu.Append(self.menu_item_news)
        self.Bind(wx.EVT_MENU, self.on_new_game, id=self.menu_item_news.GetId())
        
        self.menu_item_number_questions = wx.MenuItem(file_menu, wx.ID_ANY, "&Settings\tCtrl+S", "Indicate the number of questions to ask")
        file_menu.Append(self.menu_item_number_questions)
        self.Bind(wx.EVT_MENU, self.on_set_number_questions, id=self.menu_item_number_questions.GetId())

        file_menu.AppendSeparator()
        
        self.menu_item_help = wx.MenuItem(file_menu, wx.ID_ABOUT, "&About...\tAlt+A", "About this game")
        file_menu.Append(self.menu_item_help)
        self.Bind(wx.EVT_MENU, self.on_about, id=self.menu_item_help.GetId())

        file_menu.AppendSeparator()
        
        self.menu_item_quit = wx.MenuItem(file_menu, wx.ID_EXIT, "&Quit\tCtrl+Q", "Quit this game")
        file_menu.Append(self.menu_item_quit)
        self.Bind(wx.EVT_MENU, self.on_quit, id=self.menu_item_quit.GetId())
        
        menu_bar.Append(file_menu, "&File")
        self.SetMenuBar(menu_bar)

    def make_status_bar(self):
        self.CreateStatusBar(1)
        
    def fade_out(self):
        for alpha in range(255, -1, -25):
            self.SetTransparent(alpha)
            wx.MilliSleep(30)
        self.Destroy()

    def update_remaining_questions(self):
        """
        Updates the panel text to display the number of questions remaining.
        """
        remaining = len(self.game.questions) - self.game.current + 1
        question = (f"Remaining questions : {remaining} / {len(self.game.questions)}")
        self.remaining_questions_label.SetLabel(question)
        self.remaining_questions_pnl.Layout()

    def end_remaining_questions(self):
        remaining = 0
        self.game.current = 0
        question = (f"Remaining questions : {remaining} / {len(self.game.questions)}")
        self.remaining_questions_label.SetLabel(question)
        self.remaining_questions_pnl.Layout()
        
    def center_bitmap(self):
        """
        Centers the bitmap in the panel according to its size and that of the panel.
        """
        panel_size = self.panel.GetClientSize()
        bitmap_obj = self.bitmap.GetBitmap()
        if bitmap_obj.IsOk():
            bmp_size = bitmap_obj.GetSize()
            new_x = (panel_size.width - bmp_size.width) // 2
            new_y = (panel_size.height - bmp_size.height) // 2
            self.bitmap.SetPosition((new_x, new_y))
            self.panel.Layout()
        else:
            # If the bitmap is not yet valid, do nothing or manage otherwise
            pass

    def on_resize(self, event):
        """
        Manage panel resizing to center the bitmap.
        """
        self.center_bitmap()
        event.Skip()

    def on_set_number_questions(self, event):
        total_questions = len(self.game.images)
        dlg = wx.TextEntryDialog(
            self,
            "Enter the number of questions to ask :",
            "Settings",
            str(self.game.number_questions)
        )
        if dlg.ShowModal() == wx.ID_OK:
            try:
                new_num = int(dlg.GetValue())
                if new_num < 1:
                    wx.MessageBox("The number of questions must be greater than or equal to 1.", "Error", wx.OK | wx.ICON_ERROR)
                    return
                if new_num > total_questions:
                    wx.MessageBox(
                        f"The number entered exceeds the total number of questions ({total_questions}).",
                        "Error",
                        wx.OK | wx.ICON_ERROR
                    )
                    return
                self.game.set_number_questions(new_num)
                self.save_config(new_num)
                self.game.next_image()
                self.update_remaining_questions() 
            except ValueError:
                wx.MessageBox("Please enter a valid number.", "Error", wx.OK | wx.ICON_ERROR)

    def save_config(self, new_num):
        # Load the configuration, modify it, then save it
        try:
            with open(self.cfg_path, 'r', encoding='utf-8') as f:
                cfg = json.load(f)
        except Exception as e:
            wx.MessageBox(f"Error loading configuration file: {e}", "Error", wx.OK | wx.ICON_ERROR)
            return

        cfg['number_questions'] = new_num

        try:
            with open(self.cfg_path, 'w', encoding='utf-8') as f:
                json.dump(cfg, f, indent=4)
        except Exception as e:
            wx.MessageBox(f"Error saving configuration file: {e}", "Error", wx.OK | wx.ICON_ERROR)

    def validate_answer(self, event):
        user_answer = self.txt.GetLineText(0).lower().strip()
        if user_answer == self.answer:
            wx.MessageBox("That's right!", self.game.title, style=wx.ICON_INFORMATION)
            self.scores.append((self.answer, True))
        else:
            wx.MessageBox("You are wrong!\n\nThe correct answer was:\n{}".format(self.answer.title()), self.game.title, style=wx.ICON_EXCLAMATION)
            self.scores.append((self.answer, False))
        self.game.next_image()
        self.update_remaining_questions()
        
    def set_image(self, image, answer):
        self.bitmap.SetBitmap(image)
        self.bitmap.SetSize(image.GetSize())
        self.Layout()
        self.answer = answer
        self.txt.SetValue("")
        # Re-center the bitmap after changing the image
        self.center_bitmap()

    def on_new_game(self, event):
        # Reset the game
        self.game.reset()
        self.update_remaining_questions()  
        self.txt.SetValue("")
        self.game.next_image()

    def restart_game(self):
        if hasattr(self.game, 'reset'):
            self.game.reset()
        else:
            self.game.current = 0
            self.game.questions = []
        self.scores.clear()
        self.game.next_image()
        self.update_remaining_questions()  
        self.txt.SetValue("")

    def on_about(self, event):
        message = (
            f"""Country flags quiz game

            Version : {VERSION}
            Author : Francesc Zacarias
            Changes and improvements : Ecco

            A question/answer game where you have to find
            the names of the countries corresponding to the
            flags displayed."""
            )
        wx.MessageBox(message, "About...", wx.OK | wx.ICON_INFORMATION)

    def on_window_modal_dialog_closed(self, event):
        dialog = event.GetDialog()
        val = event.GetReturnCode()
        dialog.Destroy()

    def on_close_window(self, event):
        message = (
            f"""
            Do you really want to quit the program
            or would you like to start a new game ?"""
        )
        dlg = ConfirmationDialog(self, message, "Confirmation")
        dlg.ShowModal()
        response = dlg.result
        dlg.Destroy()

        if response == wx.ID_YES:
            # Removes the white background from the main window
            # when the game is closed (Windows only)
            self.fade_out()
            self.Destroy()
        elif response == wx.ID_NO:
            self.restart_game()
        
    def on_quit(self, event):
        self.Close()
        
#-------------------------------------------------------------------------------
        
if __name__ == '__main__':
    # Argument parser
    argp = ArgumentParser(
        description="Picture quizzes. Type the answer for each image."
    )
    argp.add_argument(
        '-c',
        '--config',
        default='config.json',
        help="Path to configuration file in JSON format (default: config.json)"
    )
    # Initial analysis
    args = argp.parse_args()

    # Loading and validating configuration
    try:
        cfg = Utils.load_config(args.config)
        Utils.validate_config(cfg)
    except Exception as e:
        print(f"Error loading or validating the configuration file: {e}")
        sys.exit(1)

    # Adding the config path to the dictionary
    cfg['cfg_path'] = args.config

    # Retrieves the default number of questions
    default_number_questions = cfg.get('number_questions', 0)

    # Adding the --number-questions argument
    argp.add_argument(
        '-n',
        '--number-questions',
        type=int,
        default=default_number_questions,
        help="Number of questions to ask (default: {})".format(default_number_questions)
    )

    # Re-parse to take into account --number-questions
    args = argp.parse_args()

    total_questions_in_cfg = len(cfg['questions'])
    if args.number_questions > total_questions_in_cfg:
        print(
            """The "number of questions" option is greater than the number of questions available: {}""".format(total_questions_in_cfg),
            file=sys.stderr
        )
        sys.exit(1)
    if args.number_questions < 0:
        print("The number of questions must be a positive number.", file=sys.stderr)
        sys.exit(1)
       
    # Launch of the wx application
    app = wx.App()
    game = Game(args.number_questions, cfg)
    app.MainLoop()
