"""
Copyright 2023-2026 Jobin Rezai

This file is part of LVNAuth.

LVNAuth 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 3 of the License, or
(at your option) any later version.

LVNAuth 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 LVNAuth.  If not, see <https://www.gnu.org/licenses/>.
"""

import sys
import pygame
import argparse
import sprite_definition as sd
import web_handler
from active_story import ActiveStory
from file_reader import FileReader
from shared_components import Passer
from animation_speed import AnimationSpeed
from datetime import datetime
from launch_window import LaunchWindow
from player_config_handler import PlayerConfigHandler
from io import BytesIO
from typing import Dict
from pygame import scrap
from pathlib import Path
from camera_handler import Camera

  

# We need to add the parent directory so
# the container_handler module will be seen.
this_module_path = Path(__file__)
one_level_up_directory = str(Path(*this_module_path.parts[0:-2]))
sys.path.append(one_level_up_directory)
from container_handler import ContainerHandler


class Main:

    def __init__(self):

        self.launch_window: LaunchWindow
        self.launch_window = None

    def show_launch_window(self, data_requester: FileReader):
        """
        Show the story's launch window, allowing the viewer to pick and choose
        which chapter or scene to play.
        
        Arguments:
        
        - data_requester: used for reading the story's info (name, etc) from
        an lvna file.
        """
        story_details = data_requester.general_header

        # Get the 'StoryInfo' dictionary, which contains details like
        # author, copyright, description, etc.
        story_info = story_details.get("StoryInfo")

        chapters_and_scenes = story_details.get("StoryChapterAndSceneNames")

        # Get the BytesIO of the poster image.
        # We're going to use this to show the poster image later.
        image_file_object = self._get_poster_image(data_requester=data_requester)
           

        # Instantiate the launch window
        self.launch_window = \
            LaunchWindow(story_info=story_info,
                         poster_file_object=image_file_object,
                         chapter_and_scene_names=chapters_and_scenes)
        
        
        # Center the launch window
        Passer.center_window_active_monitor(self.launch_window.mainwindow)

        # Run tkinter's main loop
        self.launch_window.run()

    def _get_poster_image(self, data_requester: FileReader) -> BytesIO:
        """
        Return the poster image (if any), as a BytesIO object.
        """

        # Example:
        # ['8-157434', '.png']
        range_and_file_extension = data_requester.general_header.get("PosterTitleImageLocation")

        if not range_and_file_extension:
            # There is no poster image for this visual novel.
            return

        # BytesRange object
        bytes_range = data_requester._extract_range(data_range=range_and_file_extension[0])

        # Get the image as bytes``
        bytes_data = data_requester.view[bytes_range.from_bytes:bytes_range.to_bytes]

        # Put the bytes into a BytesIO object
        file_object = BytesIO(bytes_data)

        return file_object
    
    def initialize_web(self, data_requester: FileReader):
        """
        Initialize web_handler for handling xml rpc connections for
        web-enabled visual novels. If it's not a web-enabled visual novel,
        initialize to Passer.web_handler to None.
        """
        
        story_details = data_requester.general_header

        # Get the 'StoryInfo' dictionary, which contains details like
        # author, copyright, description, etc.
        story_info = story_details.get("StoryInfo")        
        
        # bool
        web_enabled = story_info.get(web_handler.WebKeys.WEB_ACCESS.value)
        
        web_address = story_info.get(web_handler.WebKeys.WEB_ADDRESS.value)

        # If it's a shared license key, get it here.
        # If it's a private license key, we don't have it yet; this will be None
        web_key = story_info.get(web_handler.WebKeys.WEB_KEY.value)
        web_license_type =\
            story_info.get(web_handler.WebKeys.WEB_LICENSE_TYPE.value)
        
        if web_license_type == "private":
            web_license_type = web_handler.WebLicenseType.PRIVATE
            
            """
            If the user tries to play the visual novel from the current scene
            or from the startup scene, there won't be a chance to enter a
            license key. So try to get the license key from the last time the
            config file was populated.
            """
            # We won't have a license key yet, but try to get the license key 
            # from the config file in case the launch window is not used.
            web_key = Passer.player_config.get_data("LicenseKey")          
            
        else:
            web_license_type = web_handler.WebLicenseType.SHARED
            
        web_public_certificate =\
            story_info.get(web_handler.WebKeys.WEB_PUBLIC_CERTIFICATE.value)
        
        web_bypass_certificate =\
            story_info.get(web_handler.WebKeys.WEB_BYPASS_CERTIFICATE.value)
        
        # Initialize web_handler. This will be used throughout the visual
        # novel for interacting with flask and the database.
        
        # The callback method will be specified later, because the story reader
        # object hasn't been initialized yet at this point, which is where
        # the callback method is located.
        Passer.web_handler =\
            web_handler.WebHandler(
                web_key,
                web_address,
                web_public_certificate,
                web_bypass_certificate, 
                web_license_type,
                web_enabled,
                story_info.get("StoryTitle"),
                story_info.get("Episode"))

    def begin(self):
        """
        Show the launch window and then start the pygame loop.
        """
        
        # Read the .lvna file from the provided argument command switch.
        # The path to the .lvna file will be in args.file
        data_requester = FileReader(args.file)
        
        # Visual novel name and lvna full path.
        # We need both of these for the config file.        
        vn_name = data_requester.general_header.get("StoryInfo").get("StoryTitle")
        lvna_full_path = Path(args.file)
        
        # For loading and saving visual novel config data.
        Passer.player_config = PlayerConfigHandler(vn_name, lvna_full_path)        

        # Get the story's requestes window size
        # in pixels (width, height)
        screen_size = tuple(data_requester.general_header.get("StoryWindowSize"))
        
        # Start full screen?
        full_screen = data_requester.general_header.get("StoryStartFullScreen")
        if full_screen:
            # Full screen
            pg_parameters = pygame.FULLSCREEN | pygame.SCALED | pygame.RESIZABLE
        else:
            # Window-mode
            pg_parameters = pygame.SCALED | pygame.RESIZABLE

        # 'Draft' or 'Final'
        # To know whether to show the draft rectangle or not, and
        # whether to allow some keyboard shortcuts or not.
        compile_mode = data_requester.general_header.get("StoryCompileMode")
        
        # Get the visual novel title, so we can show it in the pygame 
        # caption window.
        story_title =\
            data_requester.general_header.get("StoryInfo").get("StoryTitle")
        
        draft_mode = compile_mode == "Draft"
        
        # Initialize web_handler for handling web connections
        self.initialize_web(data_requester=data_requester)

        program_running = True
        while program_running:
            
            # Initialize/reset. This gets set when the user clicks 'Play',
            # so forget any previous value of this, in case the visual novel
            # is being played again in the same instance.
            Passer.manual_startup_chapter_scene = None
    
            # Should we show the launch window?
            if show_launch:

                self.show_launch_window(data_requester=data_requester)   
                if Passer.close_after_launch_window:
                    # The player has closed the launch window,
                    # so we should exit the entire application.
                    program_running = False
                    sys.exit(0)

    
            # screen_size = (640, 480)
    
            pygame.init()
    
            clock = pygame.time.Clock()
    
            pygame.display.set_caption(f"{story_title}") #- LVNAuth Player")
    
            # The app's icon file will be either in the current directory
            # or in the 'player' directory. It depends whether the visual novel
            # is being played from the editor, or directly.
            if ContainerHandler.is_in_snap_package() or ContainerHandler.is_in_flatpak_package():
                app_icon_path = ContainerHandler.get_lvnauth_editor_icon_path_small()
            else:
                # Not in a Snap package.
    
                app_icon_path =\
                    ContainerHandler.get_absolute_path(r"./player/app_icon_small.png")
                #if not app_icon_path.exists():
                    #app_icon_path = Path(r"./player/app_icon_small.png")
    
            pygame.display.set_icon(pygame.image.load(app_icon_path))
    
            # Create the main surface
            main_surface = pygame.display.set_mode(screen_size, pg_parameters)
    
            main_surface.fill((0, 0, 0))
            
            # Create the Virtual Canvas
            # This is the buffer the story will be draw onto. 
            # It creates a blank surface the same size as the display surface.
            virtual_surface = main_surface.copy().convert_alpha()
            
            
            # Initialize the camera which will be used for zooming and panning.
            camera = Camera(screen_size=screen_size)
            
            
            # STARTING STATE: Zoomed in on a specific spot (e.g., a character's face)
            # camera.zoom = 2.05
            # camera.x = 512
            # camera.y = 189    
    
            story = ActiveStory(screen_size=screen_size,
                                data_requester=data_requester,
                                virtual_surface=virtual_surface,
                                main_surface=main_surface,
                                camera=camera, 
                                draft_mode=draft_mode)
            Passer.active_story = story
            
            # Now that the story reader object has been initialized above, use
            # on_web_request_finished() as the callback method for the server
            # replies. We couldn't specify the callback method earlier because
            # the story reader object hadn't been initialized yet.
            Passer.web_handler.callback_method_finished =\
                Passer.active_story.reader.on_web_request_finished
            
            # Delta is time in seconds since last frame.
            # Used for FPS setting independent physics.
            AnimationSpeed.delta = 0
            
            # Frames per second
            FPS = 60
            
            # Used for converting milliseconds to seconds 
            # (for delta time in seconds)
            MS_PER_SECOND = 1000
            
            # AnimationSpeed.test = 0
    
            while story.story_running:
                
                # The number of seconds elapsed in this frame
                delta_raw = clock.tick(FPS)
                AnimationSpeed.delta = delta_raw / MS_PER_SECOND            
    
                main_surface.fill((0, 0, 0))
                virtual_surface.fill((0, 0, 0))
            
                camera.update(dt=AnimationSpeed.delta)
    
                # Handle pygame events
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        story.story_running = False
                        
                        ## Without the block below, attempting to
                        ## close pygame using the window manager
                        ## will sometimes freeze pygame.
                        #pygame.display.quit()
                        #sys.exit(0)
                        # return
        
                    elif event.type == pygame.KEYDOWN:
                        self.on_key_down(event.key, event)
        
                    else:
                        story.on_event(event)
                        
                
                        
                # Check for <remote> finished requests.
                self.check_queue()
    
                # Handle movements
                story.on_loop()
    
                # Handle drawing
                story.on_render()
                
                #if not AnimationSpeed.test:
                    ## camera.start_move(target_zoom=1.5,duration=5)
                    #camera.start_move(target_x=670, target_y=220, target_zoom=3.5,duration=0)
                    ## camera.start_shake(intensity=10, duration=15.0)
                    
                    #AnimationSpeed.test = 99
                    
    
                # Shake
                # camera.start_shake(intensity=20, duration=1.0)                
    
                
                camera.apply(virtual_surface=virtual_surface,
                             main_surface=main_surface)
                
                story.on_render_dialog_rectangle(main_surface)
                
                #############################
                
                story.cover_screen_handler.draw()
        
                # Draft rectangle (to show x/y coordinates of the mouse pointer)
                if story.draft_mode:
                    story.draw_draft_rectangle()
                #############################            
            
    
                # Update the screen
                pygame.display.flip()
                
            # The pygame window is about to be closed. If there was no
            # launch window shown initially, then exit the program.
            if not show_launch:
                program_running = False
                
            pygame.quit()
            
    def check_queue(self):
        """
        Read the queue that was sent by a secondary thread.
        This method will run in the main pygame thread.
        
        This method for polling is for within pygame, not tkinter.
        This method is used for reading responses from the <remote> command.
        """
        
        if web_handler.WebHandler.the_queue.empty():
            return
        
        msg = web_handler.WebHandler.the_queue.get()
                
        self.run_remote_finished_method(msg=msg)
        
    def run_remote_finished_method(self, msg):
        """
        Run the finished method from the ServerResponseReceipt msg,
        initiated from a <remote> command.
        """
        msg.callback_method(msg)

    def on_key_down(self, key_pressed, event: pygame.event):
        """
        Handle keyboard letter presses.

        Changes:
        Nov 4, 2023 (Jobin Rezai) - Copy sprite horizontal / vertical flip values,
        but only if the values have been changed.
        """


        
        # F11
        if key_pressed == pygame.K_F11:
            pygame.display.toggle_fullscreen()
            
        # ESC
        elif key_pressed == pygame.K_ESCAPE:
            if pygame.display.is_fullscreen():
                pygame.display.toggle_fullscreen()
                
        # Alt + Enter
        elif key_pressed == pygame.K_RETURN:
            if event.mod & pygame.KMOD_ALT:
                pygame.display.toggle_fullscreen()
            else:
                # Enter by itself, no Alt. Make the story go faster.
                Passer.active_story.advance_story_or_go_faster()
                
        # Spacebar or Enter key (on num-ad)
        elif key_pressed == pygame.K_SPACE or key_pressed == pygame.K_KP_ENTER:
            Passer.active_story.advance_story_or_go_faster()

        # The following key presses only apply to draft-mode.
        if not Passer.active_story.draft_mode:
            return

        # h key
        if key_pressed == pygame.K_h:
            # Toggle the visibility of the draft rectangle.
            Passer.active_story.draft_rectangle.toggle_visibility()

        # c key
        elif key_pressed == pygame.K_c:
            """
            Copy the visible sprite locations, as commands, to the clipboard.
            """

            # The lines that will be copied to the clipboard.
            copy_info = []

            # The words to use in the commands.
            group_words = {0: "character",
                           1: "object",
                           2: "dialogue_sprite"}

            # Loop through all visible sprites in 3 sprite groups
            # and generate the X/Y commands for them.
            for idx, sprite_group in enumerate((sd.Groups.character_group,
                                                sd.Groups.object_group,
                                                sd.Groups.dialog_group)):

                # Get the word to use in the commands (ie: character, object, dialogue_sprite).
                group_word = group_words.get(idx)

                # Loop through the visible sprites in the current group we're looping on.
                sprite_name: str
                sprite_object: sd.SpriteObject
                for sprite_name, sprite_object in sprite_group.sprites.items():
                    if sprite_object.visible:
                        copy_info.append(f"# {sprite_object.general_alias}")
                        copy_info.append(
                            f"<{group_word}_set_position_x: {sprite_object.general_alias}, {sprite_object.rect.x}>")
                        copy_info.append(
                            f"<{group_word}_set_position_y: {sprite_object.general_alias}, {sprite_object.rect.y}>")

                        # Flip both directions?
                        if all([sprite_object.flipped_vertically, sprite_object.flipped_horizontally]):
                            copy_info.append(f"<{group_word}_flip_both: {sprite_object.general_alias}>")

                        # Flip only horizontally?
                        elif sprite_object.flipped_horizontally:
                            copy_info.append(f"<{group_word}_flip_horizontal: {sprite_object.general_alias}>")

                        # Flip only vertically?
                        elif sprite_object.flipped_vertically:
                            copy_info.append(f"<{group_word}_flip_vertical: {sprite_object.general_alias}>")

                        # Add an empty line
                        copy_info.append("\n")

            # Anything to copy to the clipboard?
            if copy_info:
                # Combine the list into one string
                generated_str = "\n".join(copy_info)

                # Copy the string to the clipboard
                scrap.put_text(generated_str)

                # Show a 'copied' text in the draft rectangle
                # so that the user knows it's been copied to the clipboard.
                Passer.active_story.draft_rectangle.temporary_text = "Copied sprite locations!"
 

if __name__ == "__main__":

    read_arguments = argparse.ArgumentParser()
    read_arguments.add_argument("--file",
                                dest="file",
                                type=str,
                                help="Specify an .lvna visual novel file for playing.")
    read_arguments.add_argument("--show-launch",
                                dest="show_launch",
                                action="store_true", 
                                help="Show a startup window to select a chapter or scene (Requires --file).")
    args = read_arguments.parse_args()

    # Debug for playing in the player
    if not args.file:
        
        # No --file switch was supplied, so assume draft.lvna or release.lvna

        if ContainerHandler.is_in_snap_package() \
           or ContainerHandler.is_in_flatpak_package():
            draft_path = ContainerHandler.get_draft_path()
            args.file = draft_path # str(draft_path)
        else:
            # Not inside a Snap or Flatpak
            
            # Get the full path to draft.lvna
            draft_lvna_full_path =\
                ContainerHandler.get_absolute_path(r"draft/draft.lvna")
            
            if draft_lvna_full_path.exists():
                # args.file = r"../draft/draft.lvna"
                args.file = draft_lvna_full_path
            else:
            
                # No draft file, but is there a release file?
                
                # Check if a file named 'release.lvna' exists in the current
                # path. If it exists, read that .lvna file.
                release_lvna_full_path =\
                    ContainerHandler.get_absolute_path(r"release.lvna")
                
                if release_lvna_full_path:
                    args.file = release_lvna_full_path
            
        # Show the launch window since no --file switch was supplied.
        args.show_launch = True
        
    # Make sure the file argument is a Path object 
    # so we can run .is_file() on it.
    if args.file and not isinstance(args.file, Path):
        args.file = Path(args.file)
        
    # No .lvna file specified? Or the .lvna file doesn't exist?
    if not args.file or not args.file.is_file():
        read_arguments.error("No file specified. Use --file to specify a path to a .lvna file.")
        sys.exit(0)
        
    # --show-launch can only be used if --file is specified too.
    if args.show_launch and not args.file:
        read_arguments.error("--show-launch can only be used if --file is specified.")
        sys.exit(0)

    # Should we show the launch window?
    show_launch = args.show_launch

    # print(f"{args.file=},{args.show_menu=}")

    main = Main()    
    main.begin()
