#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>

import sys
import weakref
from collections import deque
from enum import Enum
from itertools import count

from .config import build_ansi_color_table
from .constants import (
    ScreenGeometry, WindowGeometry, appname, cell_size, get_boss,
    viewport_size, wakeup
)
from .fast_data_types import (
    BRACKETED_PASTE_END, BRACKETED_PASTE_START, CELL_BACKGROUND_PROGRAM,
    CELL_FOREGROUND_PROGRAM, CELL_PROGRAM, CELL_SPECIAL_PROGRAM,
    CURSOR_PROGRAM, GRAPHICS_PROGRAM, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
    Screen, compile_program, create_cell_vao, create_graphics_vao,
    glfw_post_empty_event, init_cell_program, init_cursor_program, remove_vao,
    set_window_render_data, update_window_title, update_window_visibility
)
from .rgb import to_color
from .terminfo import get_capabilities
from .utils import color_as_int, load_shaders, parse_color_set, sanitize_title, open_url, open_cmd


class DynamicColor(Enum):
    default_fg, default_bg, cursor_color, highlight_fg, highlight_bg = range(1, 6)


DYNAMIC_COLOR_CODES = {
    10: DynamicColor.default_fg,
    11: DynamicColor.default_bg,
    12: DynamicColor.cursor_color,
    17: DynamicColor.highlight_bg,
    19: DynamicColor.highlight_fg,
}
DYNAMIC_COLOR_CODES.update({k+100: v for k, v in DYNAMIC_COLOR_CODES.items()})
window_counter = count()
next(window_counter)


def calculate_gl_geometry(window_geometry, viewport_width, viewport_height, cell_width, cell_height):
    dx, dy = 2 * cell_width / viewport_width, 2 * cell_height / viewport_height
    xmargin = window_geometry.left / viewport_width
    ymargin = window_geometry.top / viewport_height
    xstart = -1 + 2 * xmargin
    ystart = 1 - 2 * ymargin
    return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy)


def load_shader_programs():
    v, f = load_shaders('cell')
    compile_program(GRAPHICS_PROGRAM, *load_shaders('graphics'))
    for which, p in {
            'ALL': CELL_PROGRAM, 'BACKGROUND': CELL_BACKGROUND_PROGRAM, 'SPECIAL': CELL_SPECIAL_PROGRAM,
            'FOREGROUND': CELL_FOREGROUND_PROGRAM
    }.items():
        vv, ff = v.replace('WHICH_PROGRAM', which), f.replace('WHICH_PROGRAM', which)
        compile_program(p, vv, ff)
    init_cell_program()
    compile_program(CURSOR_PROGRAM, *load_shaders('cursor'))
    init_cursor_program()


def setup_colors(screen, opts):
    screen.color_profile.update_ansi_color_table(build_ansi_color_table(opts))
    screen.color_profile.set_configured_colors(*map(color_as_int, (
        opts.foreground, opts.background, opts.cursor, opts.selection_foreground, opts.selection_background)))


class Window:

    def __init__(self, tab, child, opts, args):
        self.id = next(window_counter)
        self.vao_id = create_cell_vao()
        self.gvao_id = create_graphics_vao()
        self.tab_id = tab.id
        self.tabref = weakref.ref(tab)
        self.override_title = None
        self.destroyed = False
        self.click_queue = deque(maxlen=3)
        self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0)
        self.needs_layout = True
        self.title = appname
        self.is_visible_in_layout = True
        self.child, self.opts = child, opts
        self.screen = Screen(self, 24, 80, opts.scrollback_lines, self.id)
        setup_colors(self.screen, opts)

    def __repr__(self):
        return 'Window(title={}, id={})'.format(self.title, self.id)

    def set_visible_in_layout(self, window_idx, val):
        val = bool(val)
        if val is not self.is_visible_in_layout:
            self.is_visible_in_layout = val
            update_window_visibility(self.tab_id, window_idx, val)
            if val:
                self.refresh()

    def refresh(self):
        self.screen.mark_as_dirty()
        wakeup()

    def update_position(self, window_geometry):
        self.screen_geometry = sg = calculate_gl_geometry(window_geometry, viewport_size.width, viewport_size.height, cell_size.width, cell_size.height)
        return sg

    def set_geometry(self, window_idx, new_geometry):
        if self.destroyed:
            return
        if self.needs_layout or new_geometry.xnum != self.screen.columns or new_geometry.ynum != self.screen.lines:
            boss = get_boss()
            self.screen.resize(new_geometry.ynum, new_geometry.xnum)
            current_pty_size = (
                self.screen.lines, self.screen.columns,
                max(0, new_geometry.right - new_geometry.left), max(0, new_geometry.bottom - new_geometry.top))
            sg = self.update_position(new_geometry)
            self.needs_layout = False
            boss.child_monitor.resize_pty(self.id, *current_pty_size)
        else:
            sg = self.update_position(new_geometry)
        self.geometry = g = new_geometry
        set_window_render_data(self.tab_id, window_idx, self.vao_id, self.gvao_id, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen, *g[:4])

    def contains(self, x, y):
        g = self.geometry
        return g.left <= x <= g.right and g.top <= y <= g.bottom

    def close(self):
        get_boss().close_window(self)

    def on_child_death(self):
        if self.destroyed:
            return
        self.destroyed = True
        # Remove cycles so that screen is de-allocated immediately
        boss = get_boss()
        self.screen.reset_callbacks()
        boss.gui_close_window(self)
        self.screen = None

    def write_to_child(self, data):
        if data:
            if get_boss().child_monitor.needs_write(self.id, data) is not True:
                print('Failed to write to child %d as it does not exist' % self.id, file=sys.stderr)

    # screen callbacks {{{
    def use_utf8(self, on):
        get_boss().child_monitor.set_iutf8(self.window_id, on)

    def focus_changed(self, focused):
        if focused:
            if self.screen.focus_tracking_enabled:
                self.write_to_child(b'\x1b[I')
        else:
            if self.screen.focus_tracking_enabled:
                self.write_to_child(b'\x1b[O')

    def title_changed(self, new_title):
        if self.override_title is None:
            self.title = sanitize_title(new_title or appname)
            update_window_title(self.tab_id, self.id, self.title)
            t = self.tabref()
            if t is not None:
                t.title_changed(self)
            glfw_post_empty_event()

    def icon_changed(self, new_icon):
        pass  # TODO: Implement this

    def change_colors(self, changes):
        dirtied = False

        def item(raw):
            if raw is None:
                return 0
            val = to_color(raw)
            return None if val is None else (color_as_int(val) << 8) | 2

        for which, val in changes.items():
            val = item(val)
            if val is None:
                continue
            dirtied = True
            setattr(self.screen.color_profile, which.name, val)
        if dirtied:
            self.screen.mark_as_dirty()

    def set_dynamic_color(self, code, value):
        if isinstance(value, bytes):
            value = value.decode('utf-8')
        color_changes = {}
        for val in value.split(';'):
            w = DYNAMIC_COLOR_CODES.get(code)
            if w is not None:
                if code >= 110:
                    val = None
                color_changes[w] = val
            code += 1
        self.change_colors(color_changes)
        glfw_post_empty_event()

    def set_color_table_color(self, code, value):
        cp = self.screen.color_profile
        if code == 4:
            for c, val in parse_color_set(value):
                cp.set_color(c, val)
            self.refresh()
        elif code == 104:
            if not value.strip():
                cp.reset_color_table()
            else:
                for c in value.split(';'):
                    try:
                        c = int(c)
                    except Exception:
                        continue
                    if 0 <= c <= 255:
                        cp.reset_color(c)
            self.refresh()

    def request_capabilities(self, q):
        self.write_to_child(get_capabilities(q))

    # }}}

    def text_for_selection(self):
        return ''.join(self.screen.text_for_selection())

    def destroy(self):
        if self.vao_id is not None:
            remove_vao(self.vao_id)
            remove_vao(self.gvao_id)
            self.vao_id = self.gvao_id = None

    def buffer_as_ansi(self):
        data = []
        self.screen.historybuf.as_ansi(data.append)
        self.screen.linebuf.as_ansi(data.append)
        return ''.join(data)

    def buffer_as_text(self):
        return str(self.screen.historybuf) + '\n' + str(self.screen.linebuf)

    # actions {{{

    def show_scrollback(self):
        get_boss().display_scrollback(self.buffer_as_ansi().encode('utf-8'))

    def paste(self, text):
        if text and not self.destroyed:
            if isinstance(text, str):
                text = text.encode('utf-8')
            if self.screen.in_bracketed_paste_mode:
                text = BRACKETED_PASTE_START.encode('ascii') + text + BRACKETED_PASTE_END.encode('ascii')
            self.write_to_child(text)

    def copy_to_clipboard(self):
        text = self.text_for_selection()
        if text:
            get_boss().glfw_window.set_clipboard_string(text)

    def pass_selection_to_program(self, *args):
        text = self.text_for_selection()
        if text:
            if args:
                open_cmd(args, text)
            else:
                open_url(text)

    def scroll_line_up(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_LINE, True)

    def scroll_line_down(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_LINE, False)

    def scroll_page_up(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_PAGE, True)

    def scroll_page_down(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_PAGE, False)

    def scroll_home(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_FULL, True)

    def scroll_end(self):
        if self.screen.is_main_linebuf():
            self.screen.scroll(SCROLL_FULL, False)
    # }}}
