import time

class EditCommand:
    
    "undo/redo-able representation of an art edit (eg paint, erase) operation"
    
    def __init__(self, art):
        self.art = art
        self.start_time = art.app.get_elapsed_time()
        self.finish_time = None
        # nested dictionary with frame(layer(column(row))) structure -
        # this prevents multiple commands operating on the same tile
        # from stomping each other
        self.tile_commands = {}
    
    def get_number_of_commands(self):
        commands = 0
        for frame in self.tile_commands.values():
            for layer in frame.values():
                for column in layer.values():
                    for tile in column.values():
                        commands += 1
        return commands
    
    def __str__(self):
        # get unique-ish ID from memory address
        addr = self.__repr__()
        addr = addr[addr.find('0'):-1]
        s = 'EditCommand_%s: %s tiles, time %s' % (addr, self.get_number_of_commands(),
                                                   self.finish_time)
        return s
    
    def add_command_tiles(self, new_command_tiles):
        for ct in new_command_tiles:
            # create new tables for frames/layers/columns if not present
            if not ct.frame in self.tile_commands:
                self.tile_commands[ct.frame] = {}
            if not ct.layer in self.tile_commands[ct.frame]:
                self.tile_commands[ct.frame][ct.layer] = {}
            if not ct.y in self.tile_commands[ct.frame][ct.layer]:
                self.tile_commands[ct.frame][ct.layer][ct.y] = {}
            # preserve "before" state of any command we overwrite
            if ct.x in self.tile_commands[ct.frame][ct.layer][ct.y]:
                old_ct = self.tile_commands[ct.frame][ct.layer][ct.y][ct.x]
                ct.set_before(old_ct.b_char, old_ct.b_fg, old_ct.b_bg,
                              old_ct.b_xform)
            self.tile_commands[ct.frame][ct.layer][ct.y][ct.x] = ct
    
    def undo_commands_for_tile(self, frame, layer, x, y):
        # no commands at all yet, maybe
        if len(self.tile_commands) == 0:
            return
        # tile might not have undo commands, eg text entry beyond start region
        if not y in self.tile_commands[frame][layer] or \
           not x in self.tile_commands[frame][layer][y]:
            return
        self.tile_commands[frame][layer][y][x].undo()
    
    def undo(self):
        for frame in self.tile_commands.values():
            for layer in frame.values():
                for column in layer.values():
                    for tile_command in column.values():
                        tile_command.undo()
    
    def apply(self):
        for frame in self.tile_commands.values():
            for layer in frame.values():
                for column in layer.values():
                    for tile_command in column.values():
                        tile_command.apply()


class EntireArtCommand:
    
    """
    undo/redo-able representation of a whole-art operation, eg:
    resize/crop, run art script, add/remove layer, etc
    """
    
    # art arrays to grab
    array_types = ['chars', 'fg_colors', 'bg_colors', 'uv_mods']
    
    def __init__(self, art, origin_x=0, origin_y=0):
        self.art = art
        # remember origin of resize command
        self.origin_x, self.origin_y = origin_x, origin_y
        self.before_frame = art.active_frame
        self.before_layer = art.active_layer
        self.start_time = self.finish_time = art.app.get_elapsed_time()
    
    def save_tiles(self, before=True):
        # save copies of tile data lists
        prefix = 'b' if before else 'a'
        for atype in self.array_types:
            # save list as eg "b_chars" for "character data before operation"
            src_data = getattr(self.art, atype)
            var_name = '%s_%s' % (prefix, atype)
            # deep copy each frame's data, else before == after
            new_data = []
            for frame in src_data:
                new_data.append(frame.copy())
            setattr(self, var_name, new_data)
        if before:
            self.before_size = (self.art.width, self.art.height)
        else:
            self.after_size = (self.art.width, self.art.height)
    
    def undo(self):
        # undo might remove frames/layers that were added
        self.art.set_active_frame(self.before_frame)
        self.art.set_active_layer(self.before_layer)
        if self.before_size != self.after_size:
            x, y = self.before_size
            self.art.resize(x, y, self.origin_x, self.origin_y)
        for atype in self.array_types:
            new_data = getattr(self, 'b_' + atype)
            setattr(self.art, atype, new_data[:])
        if self.before_size != self.after_size:
            # Art.resize will set geo_changed and mark all frames changed
            self.art.app.ui.adjust_for_art_resize(self.art)
        self.art.mark_all_frames_changed()
    
    def apply(self):
        if self.before_size != self.after_size:
            x, y = self.after_size
            self.art.resize(x, y, self.origin_x, self.origin_y)
        for atype in self.array_types:
            new_data = getattr(self, 'a_' + atype)
            setattr(self.art, atype, new_data[:])
        if self.before_size != self.after_size:
            self.art.app.ui.adjust_for_art_resize(self.art)
        self.art.mark_all_frames_changed()


class EditCommandTile:
    
    def __init__(self, art):
        self.art = art
        self.creation_time = self.art.app.get_elapsed_time()
        # initialize everything
        # previously did 'string list of serialized items' + setattr
        # which made prettier code but was slower
        self.frame = self.layer = self.x = self.y = None
        self.b_char = self.b_fg = self.b_bg = self.b_xform = None
        self.a_char = self.a_fg = self.a_bg = self.a_xform = None
    
    def __str__(self):
        s = 'F%s L%s %s,%s @ %.2f: ' % (self.frame, self.layer, str(self.x).rjust(2, '0'), str(self.y).rjust(2, '0'), self.creation_time)
        s += 'c%s f%s b%s x%s -> ' % (self.b_char, self.b_fg, self.b_bg, self.b_xform)
        s += 'c%s f%s b%s x%s' % (self.a_char, self.a_fg, self.a_bg, self.a_xform)
        return s
    
    def __eq__(self, value):
        return self.frame == value.frame and self.layer == value.layer and \
            self.x == value.x and self.y == value.y and \
            self.b_char == value.b_char and self.b_fg == value.b_fg and \
            self.b_bg == value.b_bg and self.b_xform == value.b_xform and \
            self.a_char == value.a_char and self.a_fg == value.a_fg and \
            self.a_bg == value.a_bg and self.a_xform == value.a_xform
    
    def copy(self):
        "returns a deep copy of this tile command"
        new_ect = EditCommandTile(self.art)
        # TODO: old or new timestamp? does it matter?
        #new_ect.creation_time = self.art.app.get_elapsed_time()
        new_ect.creation_time = self.creation_time
        # copy all properties
        new_ect.frame, new_ect.layer = self.frame, self.layer
        new_ect.x, new_ect.y = self.x, self.y
        new_ect.b_char, new_ect.b_xform = self.b_char, self.b_xform
        new_ect.b_fg, new_ect.b_bg = self.b_fg, self.b_bg
        new_ect.a_char, new_ect.a_xform = self.a_char, self.a_xform
        new_ect.a_fg, new_ect.a_bg = self.a_fg, self.a_bg
        return new_ect
    
    def set_tile(self, frame, layer, x, y):
        self.frame, self.layer = frame, layer
        self.x, self.y = x, y
    
    def set_before(self, char, fg, bg, xform):
        self.b_char, self.b_xform = char, xform
        self.b_fg, self.b_bg = fg, bg
    
    def set_after(self, char, fg, bg, xform):
        self.a_char, self.a_xform = char, xform
        self.a_fg, self.a_bg = fg, bg
    
    def is_null(self):
        return self.a_char == self.b_char and self.a_fg == self.b_fg and self.a_bg == self.b_bg and self.a_xform == self.b_xform
    
    def undo(self):
        # tile's frame or layer may have been deleted
        if self.layer > self.art.layers - 1 or self.frame > self.art.frames - 1:
            return
        if self.x >= self.art.width or self.y >= self.art.height:
            return
        tool = self.art.app.ui.selected_tool
        set_all = tool.affects_char and tool.affects_fg_color and tool.affects_fg_color and tool.affects_xform
        self.art.set_tile_at(self.frame, self.layer, self.x, self.y,
                             self.b_char, self.b_fg, self.b_bg, self.b_xform, set_all)
    
    def apply(self):
        tool = self.art.app.ui.selected_tool
        set_all = tool.affects_char and tool.affects_fg_color and tool.affects_fg_color and tool.affects_xform
        self.art.set_tile_at(self.frame, self.layer, self.x, self.y,
                             self.a_char, self.a_fg, self.a_bg, self.a_xform, set_all)


class CommandStack:
    
    def __init__(self, art):
        self.art = art
        self.undo_commands, self.redo_commands = [], []
    
    def __str__(self):
        s = 'stack for %s:\n' % self.art.filename
        s += '===\nundo:\n'
        for cmd in self.undo_commands:
            s += str(cmd) + '\n'
        s += '\n===\nredo:\n'
        for cmd in self.redo_commands:
            s += str(cmd) + '\n'
        return s
    
    def commit_commands(self, new_commands):
        self.undo_commands += new_commands[:]
        self.clear_redo()
    
    def undo(self):
        if len(self.undo_commands) == 0:
            return
        command = self.undo_commands.pop()
        self.art.app.cursor.undo_preview_edits()
        command.undo()
        self.redo_commands.append(command)
        self.art.app.cursor.update_cursor_preview()
    
    def redo(self):
        if len(self.redo_commands) == 0:
            return
        command = self.redo_commands.pop()
        # un-apply cursor preview before applying redo, else preview edits
        # edits will "stick"
        self.art.app.cursor.undo_preview_edits()
        command.apply()
        # add to end of undo stack
        self.undo_commands.append(command)
        self.art.app.cursor.update_cursor_preview()
    
    def clear_redo(self):
        self.redo_commands = []
