import platform
import sdl2
from collections import namedtuple

from ui_element import UIElement
from ui_button import UIButton, TEXT_LEFT, TEXT_CENTER, TEXT_RIGHT
from ui_colors import UIColors

from key_shifts import SHIFT_MAP


Field = namedtuple('Field', ['label', # text label for field
                             'type', # supported: str int float bool
                             'width', # width in tiles of the field
                             'oneline']) # label and field drawn on same line


# "null" field type that tells UI drawing to skip it
class SkipFieldType: pass

class ConfirmButton(UIButton):
    caption = 'Confirm'
    caption_justify = TEXT_CENTER
    width = len(caption) + 2
    dimmed_fg_color = UIColors.lightgrey
    dimmed_bg_color = UIColors.white

class CancelButton(ConfirmButton):
    caption = 'Cancel'
    width = len(caption) + 2

class OtherButton(ConfirmButton):
    "button for 3rd option in some dialogs, eg Don't Save"
    caption = 'Other'
    width = len(caption) + 2
    visible = False


class UIDialog(UIElement):
    
    tile_width, tile_height = 40, 8
    # extra lines added to height beyond contents length
    extra_lines = 0
    fg_color = UIColors.black
    bg_color = UIColors.white
    title = 'Test Dialog Box'
    # string message not tied to a specific field
    message = None
    other_button_visible = False
    titlebar_fg_color = UIColors.white
    titlebar_bg_color = UIColors.black
    fields = []
    # list of tuples of field #s for linked radio button options
    radio_groups = []
    default_field_width = 36
    default_short_field_width = int(default_field_width / 4)
    # default amount of lines padding each field
    y_spacing = 1
    active_field_fg_color = UIColors.white
    active_field_bg_color = UIColors.darkgrey
    inactive_field_fg_color = UIColors.black
    inactive_field_bg_color = UIColors.lightgrey
    # allow subclasses to override confirm caption, eg Save
    confirm_caption = None
    other_caption = None
    cancel_caption = None
    # center in window vs use tile_x/y to place
    center_in_window = True
    # checkbox char index (UI charset)
    checkbox_char_index = 131
    # radio buttons, filled and unfilled
    radio_true_char_index = 127
    radio_false_char_index = 126
    # field text set for bool fields with True value
    true_field_text = 'x'
    # if True, field labels will redraw with fields after handling input
    always_redraw_labels = False
    
    def __init__(self, ui, options):
        self.ui = ui
        # apply options, eg passed in from UI.open_dialog
        for k,v in options.items():
            setattr(self, k, v)
        self.confirm_button = ConfirmButton(self)
        self.other_button = OtherButton(self)
        self.cancel_button = CancelButton(self)
        # handle caption overrides
        def caption_override(button, alt_caption):
            if alt_caption and button.caption != alt_caption:
                button.caption = alt_caption
                button.width = len(alt_caption) + 2
        caption_override(self.confirm_button, self.confirm_caption)
        caption_override(self.other_button, self.other_caption)
        caption_override(self.cancel_button, self.cancel_caption)
        self.confirm_button.callback = self.confirm_pressed
        self.other_button.callback = self.other_pressed
        self.cancel_button.callback = self.cancel_pressed
        self.buttons = [self.confirm_button, self.other_button, self.cancel_button]
        # populate fields with text
        self.field_texts = []
        for i,field in enumerate(self.fields):
            self.field_texts.append(self.get_initial_field_text(i))
        # field cursor starts on
        self.active_field = 0
        UIElement.__init__(self, ui)
        if self.ui.menu_bar and self.ui.menu_bar.active_menu_name:
            self.ui.menu_bar.close_active_menu()
    
    def get_initial_field_text(self, field_number):
        "subclasses specify a given field's initial text here"
        return ''
    
    def get_height(self, msg_lines):
        "determine size based on contents (subclasses can use custom logic)"
        # base height = 4, titlebar + padding + buttons + padding
        h = 4
        h += 0 if len(msg_lines) == 0 else len(msg_lines) + 1
        # determine height of each field from self.fields
        for field in self.fields:
            if field.type is SkipFieldType:
                continue
            elif field.oneline or field.type is bool or field.type is None:
                h += self.y_spacing + 1
            else:
                h += self.y_spacing + 2
        h += self.extra_lines
        return h
    
    def reset_art(self, resize=True, clear_buttons=True):
        # get_message splits into >1 line if too long
        msg_lines = self.get_message() if self.message else []
        if resize:
            self.tile_height = self.get_height(msg_lines)
            self.art.resize(self.tile_width, self.tile_height)
            if self.center_in_window:
                qw, qh = self.art.quad_width, self.art.quad_height
                self.x = -(self.tile_width * qw) / 2
                self.y = (self.tile_height * qh) / 2
        # draw window
        self.art.clear_frame_layer(0, 0, self.bg_color, self.fg_color)
        s = ' ' + self.title.ljust(self.tile_width - 1)
        # invert titlebar (if kb focus)
        fg = self.titlebar_fg_color
        bg = self.titlebar_bg_color
        if not self is self.ui.keyboard_focus_element and \
           self is self.ui.active_dialog:
            fg = self.fg_color
            bg = self.bg_color
        self.art.write_string(0, 0, 0, 0, s, fg, bg)
        # message
        if self.message:
            y = 2
            for i,line in enumerate(msg_lines):
                self.art.write_string(0, 0, 2, y+i, line)
        # field caption(s)
        self.draw_fields()
        # position buttons
        self.confirm_button.x = self.tile_width - self.confirm_button.width - 2
        self.confirm_button.y = self.tile_height - 2
        if self.other_button_visible:
            self.other_button.x = self.confirm_button.x
            self.other_button.x -= self.other_button.width + 2
            self.other_button.y = self.confirm_button.y
            self.other_button.visible = True
        self.cancel_button.x = 2
        self.cancel_button.y = self.tile_height - 2
        # create field buttons so you can click em
        if clear_buttons:
            self.buttons = [self.confirm_button, self.other_button, self.cancel_button]
        for i,field in enumerate(self.fields):
            # None-type field = just a label
            if field.type is None:
                continue
            field_button = DialogFieldButton(self)
            field_button.field_number = i
            # field settings mean button can be in a variety of places
            field_button.width = 1 if field.type is bool else field.width
            field_button.x = 2 if not field.oneline or field.type is bool else len(field.label) + 1
            field_button.y = self.get_field_y(i)
            if not field.oneline:
                field_button.y += 1
            self.buttons.append(field_button)
        # draw buttons
        UIElement.reset_art(self)
    
    def update_drag(self, mouse_dx, mouse_dy):
        win_w, win_h = self.ui.app.window_width, self.ui.app.window_height
        self.x += (mouse_dx / win_w) * 2
        self.y -= (mouse_dy / win_h) * 2
        self.renderable.x, self.renderable.y = self.x, self.y
    
    def hovered(self):
        # mouse hover on focus
        if (self.ui.app.mouse_dx or self.ui.app.mouse_dy) and \
           not self is self.ui.keyboard_focus_element:
            self.ui.keyboard_focus_element = self
        self.reset_art()
    
    def update(self):
        # redraw fields every update for cursor blink
        # (seems a waste, no real perf impact tho)
        self.draw_fields(False)
        # don't allow confirmation if all field input isn't valid
        valid, reason = self.is_input_valid()
        # if input invalid, show reason in red along button of window
        bottom_y = self.tile_height - 1
        # first clear any previous warnings
        self.art.clear_line(0, 0, bottom_y)
        self.confirm_button.set_state('normal')
        # some dialogs use reason for warning + valid input
        if reason:
            fg = self.ui.error_color_index
            self.art.write_string(0, 0, 1, bottom_y, reason, fg)
        if not valid:
            self.confirm_button.set_state('dimmed')
        UIElement.update(self)
    
    def get_message(self):
        # if a triple quoted string, split line breaks
        msg = self.message.rstrip().split('\n')
        msg_lines = []
        for line in msg:
            if line != '':
                msg_lines.append(line)
        # TODO: split over multiple lines if too long
        return msg_lines
    
    def get_field_colors(self, index):
        "return FG and BG colors for field with given index"
        fg, bg = self.inactive_field_fg_color, self.inactive_field_bg_color
        # only highlight active field if we have kb focus
        if self is self.ui.keyboard_focus_element and index == self.active_field:
            fg, bg = self.active_field_fg_color, self.active_field_bg_color
        return fg, bg
    
    def get_field_label(self, field_index):
        "Subclasses can override to do custom label logic eg string formatting"
        return self.fields[field_index].label
    
    def draw_fields(self, with_labels=True):
        y = 2
        if self.message:
            y += len(self.get_message()) + 1
        for i,field in enumerate(self.fields):
            if field.type is SkipFieldType:
                continue
            x = 2
            # bool values: checkbox or radio button, always draw label to right
            if field.type is bool:
                # if field index is in any radio group, it's a radio button
                is_radio = False
                for group in self.radio_groups:
                    if i in group:
                        is_radio = True
                        break
                # true/false ~ field text is 'x'
                field_true = self.field_texts[i] == self.true_field_text
                if is_radio:
                    char = self.radio_true_char_index if field_true else self.radio_false_char_index
                else:
                    char = self.checkbox_char_index if field_true else 0
                fg, bg = self.get_field_colors(i)
                self.art.set_tile_at(0, 0, x, y, char, fg, bg)
                x += 2
            # draw label
            if field.label:
                label = self.get_field_label(i)
                if with_labels:
                    self.art.clear_line(0, 0, y)
                    self.art.write_string(0, 0, x, y, label, self.fg_color)
                if field.type in [bool, None]:
                    pass
                elif field.oneline:
                    x += len(label) + 1
                else:
                    y += 1
            # draw field contents
            if not field.type in [bool, None]:
                fg, bg = self.get_field_colors(i)
                text = self.field_texts[i]
                # caret for active field (if kb focus)
                if i == self.active_field and self is self.ui.keyboard_focus_element:
                    blink_on = int(self.ui.app.get_elapsed_time() / 250) % 2
                    if blink_on:
                        text += '_'
                # pad with spaces to full width of field
                text = text.ljust(field.width)
                self.art.write_string(0, 0, x, y, text, fg, bg)
            y += self.y_spacing + 1
    
    def get_field_y(self, field_index):
        "returns a Y value for where the given field (caption) should start"
        y = 2
        # add # of message lines
        if self.message:
            y += len(self.get_message()) + 1
        for i in range(field_index):
            if self.fields[i].oneline or self.fields[i].type in [bool, None]:
                y += self.y_spacing + 1
            else:
                y += self.y_spacing + 2
        return y
    
    def get_toggled_bool_field(self, field_index):
        field_text = self.field_texts[field_index]
        on = field_text == self.true_field_text
        # if in a radio group and turning on, toggle off the others
        radio_button = False
        for group in self.radio_groups:
            if field_index in group:
                radio_button = True
                if not on:
                    for i in group:
                        if i != field_index:
                            self.field_texts[i] = ' '
                break
        # toggle checkbox
        if not radio_button:
            return ' ' if on else self.true_field_text
        # only toggle radio button on; selecting others toggles it off
        elif on:
            return field_text
        else:
            return self.true_field_text
    
    def handle_input(self, key, shift_pressed, alt_pressed, ctrl_pressed):
        keystr = sdl2.SDL_GetKeyName(key).decode()
        field = None
        field_text = ''
        if self.active_field < len(self.fields):
            field = self.fields[self.active_field]
            field_text = self.field_texts[self.active_field]
        # special case: shortcut 'D' for 3rd button if no field input
        if len(self.fields) == 0 and keystr.lower() == 'd':
            self.other_pressed()
            return
        if keystr == '`' and not shift_pressed:
            self.ui.console.toggle()
            return
        # if list panel is up don't let user tab away
        lp = self.ui.edit_list_panel
        # only allow tab to focus shift IF list panel accepts it
        if keystr == 'Tab' and lp.is_visible() and \
           lp.list_operation in lp.list_operations_allow_kb_focus:
            self.ui.keyboard_focus_element = self.ui.edit_list_panel
            return
        elif keystr == 'Return':
            self.confirm_pressed()
        elif keystr == 'Escape':
            self.cancel_pressed()
        # cycle through fields with up/down
        elif keystr == 'Up' or (keystr == 'Tab' and shift_pressed):
            if len(self.fields) > 1:
                self.active_field -= 1
                self.active_field %= len(self.fields)
                # skip over None-type fields aka dead labels
                while self.fields[self.active_field].type is None or self.fields[self.active_field].type is SkipFieldType:
                    self.active_field -= 1
                    self.active_field %= len(self.fields)
            return
        elif keystr == 'Down' or keystr == 'Tab':
            if len(self.fields) > 1:
                self.active_field += 1
                self.active_field %= len(self.fields)
                while self.fields[self.active_field].type is None or self.fields[self.active_field].type is SkipFieldType:
                    self.active_field += 1
                    self.active_field %= len(self.fields)
            return
        elif keystr == 'Backspace':
            if len(field_text) == 0:
                pass
            # don't let user clear a bool value
            # TODO: allow for checkboxes but not radio buttons
            elif field and field.type is bool:
                pass
            elif alt_pressed:
                # for file dialogs, delete back to last slash
                last_slash = field_text[:-1].rfind('/')
                # on windows, recognize backslash as well
                if platform.system() == 'Windows':
                    last_backslash = field_text[:-1].rfind('\\')
                    if last_backslash != -1 and last_slash != -1:
                        last_slash = min(last_backslash, last_slash)
                if last_slash == -1:
                    field_text = ''
                else:
                    field_text = field_text[:last_slash+1]
            else:
                field_text = field_text[:-1]
        elif keystr == 'Space':
            # if field.type is bool, toggle value
            if field.type is bool:
                field_text = self.get_toggled_bool_field(self.active_field)
            else:
                field_text += ' '
        elif len(keystr) > 1:
            return
        # alphanumeric text input
        elif field and not field.type is bool:
            if field.type is str:
                if not shift_pressed:
                    keystr = keystr.lower()
                if not keystr.isalpha() and shift_pressed:
                    keystr = SHIFT_MAP.get(keystr, '')
            elif field.type is int and not keystr.isdigit() and keystr != '-':
                return
            # this doesn't guard against things like 0.00.001
            elif field.type is float and not keystr.isdigit() and keystr != '.' and keystr != '-':
                return
            field_text += keystr
        # apply new field text and redraw
        if field and (len(field_text) < field.width or field.type is bool):
            self.field_texts[self.active_field] = field_text
        self.draw_fields(self.always_redraw_labels)
    
    def is_input_valid(self):
        "subclasses that want to filter input put logic here"
        return True, None
    
    def dismiss(self):
        # let UI forget about us
        self.ui.active_dialog = None
        if self is self.ui.keyboard_focus_element:
            self.ui.keyboard_focus_element = None
            self.ui.refocus_keyboard()
        self.ui.elements.remove(self)
    
    def confirm_pressed(self):
        # subclasses do more here :]
        self.dismiss()
    
    def cancel_pressed(self):
        self.dismiss()
    
    def other_pressed(self):
        self.dismiss()


class DialogFieldButton(UIButton):
    
    "invisible button that provides clickability for input fields"
    
    caption = ''
    # re-set by dialog constructor
    field_number = 0
    never_draw = True
    
    def click(self):
        UIButton.click(self)
        self.element.active_field = self.field_number
        # toggle if a bool field
        if self.element.fields[self.field_number].type is bool:
            self.element.field_texts[self.field_number] = self.element.get_toggled_bool_field(self.field_number)
            # redraw fields & labels
            self.element.draw_fields(self.element.always_redraw_labels)
