import os
import sdl2
from math import ceil

from ui_element import UIElement
from art import UV_FLIPY
from key_shifts import SHIFT_MAP

from image_convert import ImageConverter
from palette import PaletteFromFile

from image_export import export_still_image, export_animation

from PIL import Image

# imports for console execution namespace - be careful!
from OpenGL import GL


CONSOLE_HISTORY_FILENAME = 'console_history'

class ConsoleCommand:
    "parent class for console commands"
    description = '[Enter a description for this command!]'
    def execute(console, args):
        return 'Test command executed.'


class QuitCommand(ConsoleCommand):
    description = 'Quit Playscii.'
    def execute(console, args):
        console.ui.app.should_quit = True


class SaveCommand(ConsoleCommand):
    description = 'Save active art, under new filename if given.'
    def execute(console, args):
        # save currently active file
        art = console.ui.active_art
        # set new filename if given
        if len(args) > 0:
            old_filename = art.filename
            art.set_filename(' '.join(args))
            art.save_to_file()
            console.ui.app.load_art_for_edit(old_filename)
            console.ui.set_active_art_by_filename(art.filename)
        else:
            art.save_to_file()
        console.ui.app.update_window_title()


class OpenCommand(ConsoleCommand):
    description = 'Open art with given filename.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: open [art filename]'
        filename = ' '.join(args)
        console.ui.app.load_art_for_edit(filename)

class RevertArtCommand(ConsoleCommand):
    description = 'Revert active art to last saved version.'
    def execute(console, args):
        console.ui.app.revert_active_art()

class LoadPaletteCommand(ConsoleCommand):
    description = 'Set the given color palette as active.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: pal [palette filename]'
        filename = ' '.join(args)
        # load AND set
        palette = console.ui.app.load_palette(filename)
        console.ui.active_art.set_palette(palette)
        console.ui.popup.set_active_palette(palette)

class LoadCharSetCommand(ConsoleCommand):
    description = 'Set the given character set as active.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: char [character set filename]'
        filename = ' '.join(args)
        charset = console.ui.app.load_charset(filename)
        console.ui.active_art.set_charset(charset)
        console.ui.popup.set_active_charset(charset)

class ImageExportCommand(ConsoleCommand):
    description = 'Export active art as PNG image.'
    def execute(console, args):
        export_still_image(console.ui.app, console.ui.active_art)

class AnimExportCommand(ConsoleCommand):
    description = 'Export active art as animated GIF image.'
    def execute(console, args):
        export_animation(console.ui.app, console.ui.active_art)

class ConvertImageCommand(ConsoleCommand):
    description = 'Convert given bitmap image to current character set + color palette.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: conv [image filename]'
        image_filename = ' '.join(args)
        ImageConverter(console.ui.app, image_filename, console.ui.active_art)
        console.ui.app.update_window_title()

class OverlayImageCommand(ConsoleCommand):
    description = 'Draw given bitmap image over active art document.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: img [image filename]'
        image_filename = ' '.join(args)
        console.ui.app.set_overlay_image(image_filename)

class ImportCommand(ConsoleCommand):
    description = 'Import file using an ArtImport class'
    def execute(console, args):
        if len(args) < 2:
            return 'Usage: imp [ArtImporter class name] [filename]'
        importers = console.ui.app.get_importers()
        importer_classname, filename = args[0], args[1]
        importer_class = None
        for c in importers:
            if c.__name__ == importer_classname:
                importer_class = c
        if not importer_class:
            console.ui.app.log("Couldn't find importer class %s" % importer_classname)
        if not os.path.exists(filename):
            console.ui.app.log("Couldn't find file %s" % filename)
        importer = importer_class(console.ui.app, filename)

class ExportCommand(ConsoleCommand):
    description = 'Export current art using an ArtExport class'
    def execute(console, args):
        if len(args) < 2:
            return 'Usage: exp [ArtExporter class name] [filename]'
        exporters = console.ui.app.get_exporters()
        exporter_classname, filename = args[0], args[1]
        exporter_class = None
        for c in exporters:
            if c.__name__ == exporter_classname:
                exporter_class = c
        if not exporter_class:
            console.ui.app.log("Couldn't find exporter class %s" % exporter_classname)
        exporter = exporter_class(console.ui.app, filename)

class PaletteFromImageCommand(ConsoleCommand):
    description = 'Convert given image into a palette file.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: getpal [image filename]'
        src_filename = ' '.join(args)
        new_pal = PaletteFromFile(console.ui.app, src_filename, src_filename)
        if not new_pal.init_success:
            return
        #console.ui.app.load_palette(new_pal.filename)
        console.ui.app.palettes.append(new_pal)
        console.ui.active_art.set_palette(new_pal)
        console.ui.popup.set_active_palette(new_pal)

class SetGameDirCommand(ConsoleCommand):
    description = 'Load game from the given folder.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: setgame [game dir name]'
        game_dir_name = ' '.join(args)
        console.ui.app.gw.set_game_dir(game_dir_name, True)

class LoadGameStateCommand(ConsoleCommand):
    description = 'Load the given game state save file.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: game [game state filename]'
        gs_name = ' '.join(args)
        console.ui.app.gw.load_game_state(gs_name)

class SaveGameStateCommand(ConsoleCommand):
    description = 'Save the current game state as the given filename.'
    def execute(console, args):
        "Usage: savegame [game state filename]"
        gs_name = ' '.join(args)
        console.ui.app.gw.save_to_file(gs_name)

class SpawnObjectCommand(ConsoleCommand):
    description = 'Spawn an object of the given class name.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: spawn [class name]'
        class_name = ' '.join(args)
        console.ui.app.gw.spawn_object_of_class(class_name)

class CommandListCommand(ConsoleCommand):
    description = 'Show the list of console commands.'
    def execute(console, args):
        # TODO: print a command with usage if available
        console.ui.app.log('Commands:')
        # alphabetize command list
        command_list = list(commands.keys())
        command_list.sort()
        for command in command_list:
            desc = commands[command].description
            console.ui.app.log(' %s - %s' % (command, desc))

class RunArtScriptCommand(ConsoleCommand):
    description = 'Run art script with given filename on active art.'
    def execute(console, args):
        if len(args) == 0:
            return 'Usage: src [art script filename]'
        filename = ' '.join(args)
        console.ui.active_art.run_script(filename)

class RunEveryArtScriptCommand(ConsoleCommand):
    description = 'Run art script with given filename on active art at given rate.'
    def execute(console, args):
        if len(args) < 2:
            return 'Usage: srcev [rate] [art script filename]'
        rate = float(args[0])
        filename = ' '.join(args[1:])
        console.ui.active_art.run_script_every(filename, rate)
        # hide so user can immediately see what script is doing
        console.hide()

class StopArtScriptsCommand(ConsoleCommand):
    description = 'Stop all actively running art scripts.'
    def execute(console, args):
        console.ui.active_art.stop_all_scripts()

# map strings to command classes for ConsoleUI.parse
commands = {
    'exit': QuitCommand,
    'quit': QuitCommand,
    'save': SaveCommand,
    'open': OpenCommand,
    'char': LoadCharSetCommand,
    'pal': LoadPaletteCommand,
    'imgexp': ImageExportCommand,
    'animexport': AnimExportCommand,
    'conv': ConvertImageCommand,
    'getpal': PaletteFromImageCommand,
    'setgame': SetGameDirCommand,
    'game': LoadGameStateCommand,
    'savegame': SaveGameStateCommand,
    'spawn': SpawnObjectCommand,
    'help': CommandListCommand,
    'scr': RunArtScriptCommand,
    'screv': RunEveryArtScriptCommand,
    'scrstop': StopArtScriptsCommand,
    'revert': RevertArtCommand,
    'img': OverlayImageCommand,
    'imp': ImportCommand,
    'exp': ExportCommand
}


class ConsoleUI(UIElement):
    
    visible = False
    snap_top = True
    snap_left = True
    # how far down the screen the console reaches when visible
    height_screen_pct = 0.75
    # how long (seconds) to shift/fade into view when invoked
    show_anim_time = 0.75
    bg_alpha = 0.75
    prompt = '>'
    # _ ish char
    bottom_line_char_index = 76
    right_margin = 3
    # transient, but must be set here b/c UIElement.init calls reset_art
    current_line = ''
    game_mode_visible = True
    all_modes_visible = True
    
    def __init__(self, ui):
        self.bg_color_index = ui.colors.darkgrey
        self.highlight_color = 8 # yellow
        UIElement.__init__(self, ui)
        # state stuff for console move/fade
        self.alpha = 0
        self.target_alpha = 0
        self.target_y = 2
        # start off top of screen
        self.renderable.y = self.y = 2
        # user input and log
        self.last_lines = []
        self.history_filename = self.ui.app.config_dir + CONSOLE_HISTORY_FILENAME
        if os.path.exists(self.history_filename):
            self.history_file = open(self.history_filename, 'r')
            try:
                self.command_history = self.history_file.readlines()
            except:
                self.command_history = []
            self.history_file = open(self.history_filename, 'a')
        else:
            self.history_file = open(self.history_filename, 'w+')
            self.command_history = []
        self.history_index = 0
        # junk data in last user line so it changes on first update
        self.last_user_line = 'test'
        # max line length = width of console minus prompt + _
        self.max_line_length = int(self.art.width) - self.right_margin
    
    def reset_art(self):
        self.width = ceil(self.ui.width_tiles * self.ui.scale)
        # % of screen must take aspect into account
        inv_aspect = self.ui.app.window_height / self.ui.app.window_width
        self.height = int(self.ui.height_tiles * self.height_screen_pct * inv_aspect * self.ui.scale)
        # dim background
        self.renderable.bg_alpha = self.bg_alpha
        # must resize here, as window width will vary
        self.art.resize(self.width, self.height)
        self.max_line_length = int(self.width) - self.right_margin
        self.text_color = self.ui.palette.lightest_index
        self.clear()
        # truncate current user line if it's too long for new width
        self.current_line = self.current_line[:self.max_line_length]
        #self.update_user_line()
        # empty log lines so they refresh from app
        self.last_user_line = 'XXtestXX'
        self.last_lines = []
    
    def toggle(self):
        if self.visible:
            self.hide()
        else:
            self.show()
    
    def show(self):
        self.visible = True
        self.target_alpha = 1
        self.target_y = 1
        self.ui.menu_bar.visible = False
        self.ui.pulldown.visible = False
    
    def hide(self):
        self.target_alpha = 0
        self.target_y = 2
        if self.ui.app.can_edit:
            self.ui.menu_bar.visible = True
    
    def update_loc(self):
        # TODO: this lerp is super awful, simpler way based on dt?
        # TODO: use self.show_anim_time instead of this garbage!
        speed = 0.25
        
        if self.y > self.target_y:
            self.y -= speed
        elif self.y < self.target_y:
            self.y += speed
        if abs(self.y - self.target_y) < speed:
            self.y = self.target_y
        
        if self.alpha > self.target_alpha:
            self.alpha -= speed / 2
        elif self.alpha < self.target_alpha:
            self.alpha += speed / 2
        if abs(self.alpha - self.target_alpha) < speed:
            self.alpha = self.target_alpha
        if self.alpha == 0:
            self.visible = False
        
        self.renderable.y = self.y
        self.renderable.alpha = self.alpha
    
    def clear(self):
        self.art.clear_frame_layer(0, 0, self.bg_color_index)
        # line -1 is always a line of ____________
        for x in range(self.width):
            self.art.set_tile_at(0, 0, x, -1, self.bottom_line_char_index, self.text_color, None, UV_FLIPY)
    
    def update_user_line(self):
        "draw current user input on second to last line, with >_ prompt"
        # clear entire user line first
        self.art.write_string(0, 0, 0, -2, ' ' * self.width, self.text_color)
        self.art.write_string(0, 0, 0, -2, '%s ' % self.prompt, self.text_color)
        # if first item of line is a valid command, change its color
        items = self.current_line.split()
        if len(items) > 0 and items[0] in commands:
            self.art.write_string(0, 0, 2, -2, items[0], self.highlight_color)
            offset = 2 + len(items[0]) + 1
            args = ' '.join(items[1:])
            self.art.write_string(0, 0, offset, -2, args, self.text_color)
        else:
            self.art.write_string(0, 0, 2, -2, self.current_line, self.text_color)
        # draw underscore for caret at end of input string
        x = len(self.prompt) + len(self.current_line) + 1
        i = self.ui.charset.get_char_index('_')
        self.art.set_char_index_at(0, 0, x, -2, i)
    
    def update_log_lines(self):
        "update art from log lines"
        log_index = -1
        for y in range(self.height - 3, -1, -1):
            try:
                line = self.ui.app.logger.lines[log_index]
            except IndexError:
                break
            # trim line to width of console
            if len(line) >= self.max_line_length:
                line = line[:self.max_line_length]
            self.art.write_string(0, 0, 1, y, line, self.text_color)
            log_index -= 1
    
    def update(self):
        "update our Art with the current console log lines + user input"
        self.update_loc()
        if not self.visible:
            return
        # check for various early out scenarios, updating all chars every frame
        # gets expensive
        user_input_changed = self.last_user_line != self.current_line
        log_changed = self.last_lines != self.ui.app.logger.lines
        # remember log & user lines, bail early next update if no change
        self.last_lines = self.ui.app.logger.lines[:]
        self.last_user_line = self.current_line
        if not user_input_changed and not log_changed:
            return
        # if log lines changed, clear all tiles to shift in new text
        if log_changed:
            self.clear()
            self.update_log_lines()
            # if user pressed enter on a blank line, user_input_changed will
            # be False but we should still update
            self.update_user_line()
        # update user line independently of log, it changes at a different rate
        if user_input_changed:
            self.update_user_line()
    
    def visit_command_history(self, index):
        if len(self.command_history) == 0:
            return
        self.history_index = index
        self.history_index %= len(self.command_history)
        self.current_line = self.command_history[self.history_index].strip()
    
    def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
        "handles a key from Application.input"
        keystr = sdl2.SDL_GetKeyName(key).decode()
        # TODO: get console bound key from InputLord, detect that instead of
        # hard-coded backquote
        if keystr == '`':
            self.toggle()
            return
        elif keystr == 'Return':
            line = '%s %s' % (self.prompt, self.current_line)
            self.ui.app.log(line)
            # if command is same as last, don't repeat it
            if len(self.command_history) == 0 or (len(self.command_history) > 0 and self.current_line != self.command_history[-1]):
                # don't add blank lines to history
                if self.current_line.strip():
                    self.command_history.append(self.current_line)
                    self.history_file.write(self.current_line + '\n')
            self.parse(self.current_line)
            self.current_line = ''
            self.history_index = 0
        elif keystr == 'Tab':
            # TODO: autocomplete (commands, filenames)
            pass
        elif keystr == 'Up':
            # page back through command history
            self.visit_command_history(self.history_index - 1)
        elif keystr == 'Down':
            # page forward through command history
            self.visit_command_history(self.history_index + 1)
        elif keystr == 'Backspace' and len(self.current_line) > 0:
            # alt-backspace: delete to last delimiter, eg periods
            if alt_pressed:
                # "index to delete to"
                delete_index = -1
                for delimiter in delimiters:
                    this_delimiter_index = self.current_line.rfind(delimiter)
                    if this_delimiter_index > delete_index:
                        delete_index = this_delimiter_index
                if delete_index > -1:
                    self.current_line = self.current_line[:delete_index]
                else:
                    self.current_line = ''
                    # user is bailing on whatever they were typing,
                    # reset position in cmd history
                    self.history_index = 0
            else:
                self.current_line = self.current_line[:-1]
                if len(self.current_line) == 0:
                    # same as above: reset position in cmd history
                    self.history_index = 0
        elif keystr == 'Space':
            keystr = ' '
        # ignore any other non-character keys
        if len(keystr) > 1:
            return
        if keystr.isalpha() and not shift_pressed:
            keystr = keystr.lower()
        elif not keystr.isalpha() and shift_pressed:
            keystr = SHIFT_MAP.get(keystr, '')
        if len(self.current_line) < self.max_line_length:
            self.current_line += keystr
    
    def parse(self, line):
        # is line in a list of know commands? if so, handle it.
        items = line.split()
        output = None
        if len(items) == 0:
            pass
        elif items[0] in commands:
            cmd = commands[items[0]]
            output = cmd.execute(self, items[1:])
        else:
            # if not, try python eval, give useful error if it fails
            try:
                # set some locals for easy access from eval
                ui = self.ui
                app = ui.app
                camera = app.camera
                art = ui.active_art
                player = app.gw.player
                sel = None if len(app.gw.selected_objects) == 0 else app.gw.selected_objects[0]
                world = app.gw
                hud = app.gw.hud
                # special handling of assignment statements, eg x = 3:
                # detect strings that pattern-match, send them to exec(),
                # send all other strings to eval()
                eq_index = line.find('=')
                is_assignment = eq_index != -1 and line[eq_index+1] != '='
                if is_assignment:
                    exec(line)
                else:
                    output = str(eval(line))
            except Exception as e:
                # try to output useful error text
                output = '%s: %s' % (e.__class__.__name__, str(e))
        # commands CAN return None, so only log if there's something
        if output and output != 'None':
            self.ui.app.log(output)
    
    def destroy(self):
        self.history_file.close()


# delimiters - alt-backspace deletes to most recent one of these
delimiters = [' ', '.', ')', ']', ',', '_']
