import curses
import random
import time
from typing import Any
import pygame
import multiprocessing
import signal


class Game:

    def __init__(self, stdscr, queue):
        # Get curses standard screen
        self.stdscr = stdscr

        # Multiprocessing Queue
        self.queue = queue

        # Defaults
        self.snake_length = 3
        self.snake_speed = 7
        self.enable_effects = True
        self.enable_music = False
        self.music_started_once = False

        # Snake and food characters
        # ASCII Table: https://theasciicode.com.ar/
        self.head_char = '█'
        #head_char = '■'
        #head_char = '0'
        #head_char_up = '▄'
        #head_char_down = '▀'
        self.body_char = '▓'
        #body_char = 'Φ'
        #body_char = '■'
        #body_char = 'O'
        #body_char = '░'
        #body_char = '■'
        #food_char = '💗'
        self.food_char = '█'
        #food_char = '▓'
        #food_char = '▒'


    def load(self):

        # Setup colors
        curses.start_color()
        self.colors = {
            'green': 1,
            'red': 2,
            'white': 3
        }
        curses.init_pair(self.colors['green'], curses.COLOR_GREEN, curses.COLOR_BLACK) # Snake
        curses.init_pair(self.colors['red'], curses.COLOR_RED, curses.COLOR_BLACK)   # Food
        curses.init_pair(self.colors['white'], curses.COLOR_WHITE, curses.COLOR_BLACK) # Score/Text
        self.stdscr.bkgd(' ', curses.color_pair(self.colors['white']))

        curses.set_escdelay(1)

        # Get terminal (screen) height and width
        self.screen_height, self.screen_width = self.stdscr.getmaxyx()

        # Show selcome page
        self.effect('start')
        self.load_welcome()

        # Ask for snake length
        self.snake_length = self.ask_question(f"Snake Length (1 to {self.screen_width - 5}, Default {self.snake_length}): ", int, 3, 3, self.screen_width - 5)

        # Ask for snake speed
        self.snake_speed = self.ask_question(f"Snake Speed (1 to 10, Default {self.snake_speed}): ", int, 7, 1, 10)

        # Ask for Music
        self.enable_music = (self.ask_question(f"Turn on Music (y/N): ", str, 'N')).upper() == 'Y'

        # Ask for Effects
        self.enable_effects = (self.ask_question(f"Turn on Sound Effects (Y/n): ", str, 'Y')).upper() == 'Y'

        # Debug Inputs
        #self.print_center(f"Length: {self.snake_length}, Speed: {self.snake_speed}, Width: {self.screen_width}, Height: {self.screen_height}")
        #time.sleep(2)

        # Start music
        if self.enable_music:
            if self.music_started_once:
                self.music_unpause()
            else:
                self.music_start()
        else:
            self.music_pause()

        # Start game!!!
        self.start_game()


    def start_game(self):
        # Hide cursor
        self.hide_cursor()

        # Non-blocking getch (get character)
        self.stdscr.nodelay(1)

        # Calculate snake speed
        wait_for_input_ms = {
            1: 250, 2: 225, 3: 200,
            4: 175, 5: 150, 6: 125,
            7: 100, 8: 75,  9: 50, 10: 25
        }
        self.stdscr.timeout(wait_for_input_ms[self.snake_speed])

        # Ensure window is large enough
        if self.screen_height < 10 or self.screen_width < 20:
            self.print_center("Terminal window is too small!")
            time.sleep(2)
            exit()

        # Start the head at x=self.snake_length so the tail ends at x=1
        snake = [[self.screen_height // 2, self.snake_length - i] for i in range(self.snake_length)]

        # Initial food
        def get_new_food():
            while True:
                nf = [random.randint(1, self.screen_height - 2), random.randint(1, self.screen_width - 2)]
                if nf not in snake:
                    return nf

        food = get_new_food()

        # Initial direction
        key = curses.KEY_RIGHT
        score = self.snake_length

        while True:
            next_key = self.stdscr.getch()
            # If no key is pressed, next_key is -1
            # Update direction if it's a valid arrow key and not opposite to current
            if next_key != -1:
                # DOWN
                if next_key == curses.KEY_DOWN and key != curses.KEY_UP:
                    key = next_key
                # UP
                elif next_key == curses.KEY_UP and key != curses.KEY_DOWN:
                    key = next_key
                # LEFT
                elif next_key == curses.KEY_LEFT and key != curses.KEY_RIGHT:
                    key = next_key
                # RIGHT
                elif next_key == curses.KEY_RIGHT and key != curses.KEY_LEFT:
                    key = next_key
                # ESCAPE Menu
                elif next_key == 27:
                    previous_key = key
                    if self.enable_music: self.music_pause()
                    self.print_center(f"GAME PAUSED...", 0, self.colors['red'])
                    self.print_center(f"ESC Resume, (R)estart, (Q)uit, (M)enu", 2, self.colors['green'])
                    while True:
                        key = self.stdscr.getch()
                        if key == 27:
                            break
                        elif key == ord('r'):
                            if self.enable_music: self.music_unpause()
                            self.start_game()
                            return
                        elif key == ord('q'):
                            self.exit_game()
                        elif key == ord('m'):
                            self.load()
                            return
                    # Resume
                    key = previous_key
                    if self.enable_music: self.music_unpause()
                    continue
                # (R)estart
                elif next_key == ord('r'):
                    self.start_game()
                    return
                # # (Q)uit
                # elif next_key == ord('q'):
                #     break

            # Calculate new head
            head = snake[0]
            if key == curses.KEY_DOWN:
                new_head = [head[0] + 1, head[1]]
            elif key == curses.KEY_UP:
                new_head = [head[0] - 1, head[1]]
            elif key == curses.KEY_LEFT:
                new_head = [head[0], head[1] - 1]
            elif key == curses.KEY_RIGHT:
                new_head = [head[0], head[1] + 1]
            else:
                new_head = [head[0], head[1]]
                pass
                # Should not happen
                #exit('yyyyy')
                #new_head = head

            # Check wall collisions
            if (new_head[0] in [0, self.screen_height - 1] or
                new_head[1] in [0, self.screen_width - 1]):
                # Crashed into wall, quit
                self.effect('wall')
                break

            # Check self collisions
            if new_head in snake:
                # Crashed into self, quit
                self.effect('self')
                break

            snake.insert(0, new_head)

            # Check if snake ate food
            if snake[0] == food:
                self.effect('eat')
                score += 1
                food = get_new_food()
            else:
                snake.pop() # Remove tail

            # Draw everything
            self.clear_screen()
            self.stdscr.attron(curses.color_pair(3))
            self.stdscr.border(0)
            self.stdscr.attroff(curses.color_pair(3))

            # Draw snake
            for i, (y, x) in enumerate(snake):
                char = self.head_char if i == 0 else self.body_char
                self.stdscr.addch(y, x, char, curses.color_pair(1))

            # Draw food
            self.stdscr.addch(food[0], food[1], self.food_char, curses.color_pair(2))

            # Draw score
            self.stdscr.addstr(0, 2, f' Length: {score} @ {self.snake_speed}MPH ({self.screen_width}x{self.screen_height}) ', curses.color_pair(3))

            self.refresh_screen()

        # Game Over screen
        self.effect('over')
        self.print_center(f"GAME OVER! SCORE: {score}", 0, self.colors['red'])
        self.print_center(f"(R)estart, (Q)uit, (M)enu", 2, self.colors['green'])
        self.stdscr.nodelay(0)
        #if self.enable_music: self.music_pause()
        while True:
            key = self.stdscr.getch()
            if key == ord('r'):
                #if self.enable_music: self.music_unpause()
                self.start_game()
                return
            elif key == ord('q'):
                # Game over, exit app!
                self.exit_game()
            elif key == ord('m'):
                self.load()
                return


    def print_center(self, value, y_offset = 0, color = 3):
        # Calculate center position
        y = self.screen_height // 2
        x = (self.screen_width - len(value)) // 2
        if x < 0: x = 1
        self.stdscr.addstr(int(y + y_offset), int(x), value, curses.color_pair(color))
        self.refresh_screen()


    def ask_question(self, question: str, value_type: Any, value_default: Any, value_min: int = 0, value_max: int = 0) -> Any:
        # Make the cursor visible
        curses.curs_set(1)

        # Calculate center position of prompt
        y = self.screen_height // 2
        x = (self.screen_width - len(question)) // 2
        if x < 0: x = 1

        # Display question and wait for input + ENTER key to finish
        self.clear_screen()
        self.stdscr.addstr(y, x, question)
        self.refresh_screen()
        curses.noecho()
        curses.cbreak()

        # Wait for answer with ENTER key to end input
        answer = ""
        while True:
            key = self.stdscr.getch()

            # Check if the Enter key (ASCII 10 or 13) was pressed
            if key == curses.KEY_ENTER or key in [10, 13]:
                break
            # Handle backspace
            elif key == curses.KEY_BACKSPACE or key == 127:
                if len(answer) > 0:
                    answer = answer[:-1]
                    # Erase the character from the screen
                    self.stdscr.addch('\b')
                    self.stdscr.addch(' ')
                    self.stdscr.addch('\b')
            # Handle regular characters (check for printable ASCII range if needed)
            elif 32 <= key <= 126:
                answer += chr(key)
                self.stdscr.addch(key) # Manually echo the character

            self.refresh_screen()

        # Validate input
        if value_type == int:
            try:
                answer = int(answer)
            except Exception as e:
                answer = value_default
            if answer < value_min or answer > value_max:
                # Invalid integer, ask again
                self.print_center(f"ERROR: Must be an integer between {value_min} and {value_max}", 2, self.colors['red'])
                time.sleep(2)
                self.ask_question(question, value_type, value_min, value_max)
        else:
            if not answer:
                answer = value_default

        return answer


    def load_welcome(self):
        self.hide_cursor()
        self.clear_screen()
        self.print_center("█▓▒░ Welcome to Curses Python!!! ░▒▓█", 0, self.colors['red'])
        self.print_center("by mReschke Productions", 2, self.colors['green'])
        time.sleep(2)


    def hide_cursor(self):
        curses.curs_set(0)


    def show_cursor(self):
        curses.curs_set(1)


    def clear_screen(self):
        self.stdscr.clear()


    def refresh_screen(self):
        self.stdscr.refresh()

    def exit_game(self):
        self.queue.put('quit')
        exit()

    def effect(self, effect):
        if self.enable_effects:
            self.queue.put(effect)

    def music_start(self):
        self.music_started_once = True
        self.queue.put('music.start')

    def music_stop(self):
        self.queue.put('music.stop')

    def music_pause(self):
        self.queue.put('music.pause')

    def music_unpause(self):
        self.queue.put('music.unpause')


def main(stdscr, queue):
    game = Game(stdscr, queue)
    game.load()


def sound_process(sound_queue):
    # Initialize pygame mixer in the new process
    pygame.mixer.init()

    effects = {
        'eat': pygame.mixer.Sound('effects/bing.mp3'),
        'wall': pygame.mixer.Sound('effects/wall.mp3'),
        'self': pygame.mixer.Sound('effects/cut.mp3'),
        'over': pygame.mixer.Sound('effects/over.mp3'),
        'start': pygame.mixer.Sound('effects/start2.mp3'),
    }

    # Load sounds/music (adjust file paths as needed)
    # Use .wav or .ogg for better compatibility
    #effect = pygame.mixer.Sound('effects/bing.mp3')
    #music_file = 'music/QonsVivaEveryone.mp3'
    #music_file = 'music/Aztec.mp3'
    #music_file = 'music/Snug_Neon_Artery.mp3'
    music_file = 'music/bubbles.ogg'
    #music_file = 'music/s.mp3'

    while True:
        if not sound_queue.empty():
            command = sound_queue.get()

            # Play effects if requested
            for effect, sound in effects.items():
                if command == effect:
                    sound.play()

            # Play music
            if command == 'music.start':
                pygame.mixer.music.load(music_file)
                pygame.mixer.music.set_volume(0.5)
                pygame.mixer.music.play(-1) # Play music indefinitely
            elif command == 'music.stop':
                pygame.mixer.music.stop()
            elif command == 'music.pause':
                pygame.mixer.music.pause();
            elif command == 'music.unpause':
                pygame.mixer.music.unpause();

            # Quit came
            elif command == 'quit':
                break

        # Small delay to prevent high CPU usage
        time.sleep(0.1)


if __name__ == "__main__":

    try:

        # Handle CTRL+C (uvicore also handles with Aborted! but this catches other odd cases)
        signal.signal(signal.SIGINT, lambda sig, frame: exit())

        # Create a queue for communication
        queue = multiprocessing.Queue()

        # Start the sound process
        p = multiprocessing.Process(target=sound_process, args=(queue,))
        p.start()

        # Start curses game loop
        game = curses.wrapper(main, queue)
    except KeyboardInterrupt:
        exit(0)


