#!/usr/bin/env python2
# emacs this is -*-python-*-

import time
from ctypes import byref, cast
from ctypes import POINTER, c_int, c_uint32, c_char

from pyxtrlock import panic
try:
    import pyxtrlock.xcb as xcb
except ImportError as err:
    panic(err)

try:
    import pyxtrlock.X as X
except ImportError as err:
    panic(err)

def create_cursor(cursor, conn, screen, window):
    csr_map = xcb.image_create_pixmap_from_bitmap_data(conn, window,
                                                       cursor["fg_bitmap"],
                                                       cursor["width"],
                                                       cursor["height"],
                                                       1, 0, 0, None)
    csr_mask = xcb.image_create_pixmap_from_bitmap_data(conn, window,
                                                        cursor["bg_bitmap"],
                                                        cursor["width"],
                                                        cursor["height"],
                                                        1, 0, 0, None)

    if cursor["color_mode"] == "named":
        csr_bg = xcb.alloc_named_color_sync(conn, screen.default_colormap,
                                            cursor["bg_color"])
        csr_fg = xcb.alloc_named_color_sync(conn, screen.default_colormap,
                                            cursor["fg_color"])
    elif cursor["color_mode"] == "rgb":
        r, g, b = cursor["bg_color"]
        csr_bg = xcb.alloc_color_sync(conn, screen.default_colormap, r, g, b)
        r, g, b = cursor["fg_color"]
        csr_fg = xcb.alloc_color_sync(conn, screen.default_colormap, r, g, b)
    else:
        panic("Invalid color mode")
    try:
        cursor = xcb.create_cursor_sync(conn, csr_map, csr_mask,
                                        csr_fg, csr_bg,
                                        cursor["x_hot"], cursor["y_hot"])
    except xcb.XCBError:
        panic("pyxtrlock: Could not create cursor")
    return cursor


def lock(pw_verifier):

    display = X.create_window(None)
    conn = X.get_xcb_connection(display)

    if not display:
        panic("pyxtrlock: Could not connect to X server")

    screen_num = c_int()

    setup = xcb.get_setup(conn)

    iter_ = xcb.setup_roots_iterator(setup)

    while screen_num.value:
        xcb.screen_next(byref(iter_))
        screen_num.value -= 1

    screen = iter_.data.contents

    # create window
    window = xcb.generate_id(conn)

    attribs = (c_uint32 * 2)(1, xcb.EVENT_MASK_KEY_PRESS)
    xcb.create_window(conn, xcb.COPY_FROM_PARENT, window, screen.root,
                      0, 0, 1, 1, 0, xcb.WINDOW_CLASS_INPUT_ONLY,
                      xcb.VisualID(xcb.COPY_FROM_PARENT),
                      xcb.CW_OVERRIDE_REDIRECT | xcb.CW_EVENT_MASK,
                      cast(byref(attribs), POINTER(c_uint32)))

    cursor = create_cursor({
        "width": 1,
        "height": 1,
        "x_hot": 1,
        "y_hot": 1,
        "fg_bitmap": bytes([0x00]),
        "bg_bitmap": bytes([0x00]),
        "color_mode": "named",
        "bg_color": "steelblue3",
        "fg_color": "grey25"
    }, conn, screen, window)

    # map window
    xcb.map_window(conn, window)

    # Grab keyboard
    try:
        status = xcb.grab_keyboard_sync(conn, 0, window, xcb.CURRENT_TIME,
                                        xcb.GRAB_MODE_ASYNC, xcb.GRAB_MODE_ASYNC)

        if status != xcb.GrabSuccess:
            panic("pyxtrlock: Could not grab keyboard")
    except xcb.XCBError as e:
        panic("pyxtrlock: Could not grab keyboard")

    # Grab pointer
    # Use the method from the original xtrlock code:
    #  "Sometimes the WM doesn't ungrab the keyboard quickly enough if
    #  launching xtrlock from a keystroke shortcut, meaning xtrlock fails
    #  to start We deal with this by waiting (up to 100 times) for 10,000
    #  microsecs and trying to grab each time. If we still fail
    #  (i.e. after 1s in total), then give up, and emit an error"
    for i in range(100):
        try:
            status = xcb.grab_pointer_sync(conn, False, window, 0,
                                        xcb.GRAB_MODE_ASYNC,
                                        xcb.GRAB_MODE_ASYNC,
                                        xcb.WINDOW_NONE, cursor,
                                        xcb.CURRENT_TIME)

            if status == xcb.GrabSuccess:
                break
            else:
                time.sleep(0.01)
        except xcb.XCBError as e:
            time.sleep(0.01)
    else:
        panic("pyxtrlock: Could not grab pointing device")

    xcb.flush(conn)

    # Prepare X Input
    im = X.open_IM(display, None, None, None)
    if not im:
        panic("pyxtrlock: Could not open Input Method")

    ic = X.create_IC(im, X.N_INPUT_STYLE,
                    X.IM_PRE_EDIT_NOTHING | X.IM_STATUS_NOTHING, None)
    if not ic:
        panic("pyxtrlock: Could not open Input Context")

    X.set_ic_focus(ic)

    # pwd length limit to prevent memory exhaustion (and therefore
    # possible failure due to OOM killing)
    PWD_LENGTH_LIMIT = 100 * 1024

    # timeout algorithm constants
    TIMEOUTPERATTEMPT = 2000
    MAXGOODWILL = TIMEOUTPERATTEMPT * 5
    INITIALGOODWILL = MAXGOODWILL
    GOODWILLPORTION = 0.3

    # main event loop
    pwd = []
    timeout = 0
    goodwill = INITIALGOODWILL
    while True:
        with xcb.wait_for_event(conn) as event:
            if event.contents.response_type == xcb.KEY_PRESS:
                xcb_key_press_event = cast(event,
                                        POINTER(xcb.KeyPressEvent)).contents
                time_stamp = xcb_key_press_event.time
                if time_stamp < timeout:
                    continue

                x_key_press_event = X.KeyEvent.from_xcb_event(display,
                                                            xcb_key_press_event)

                status = X.Status()
                keysym = X.Keysym()
                size = 0
                buf = bytearray(size)

                length = X.utf8_lookup_string(ic, byref(x_key_press_event), None,
                                            size, byref(keysym), byref(status))
                if status.value == X.BUFFER_OVERFLOW:
                    buf = bytearray(length)
                    buf_p = cast((c_char * length).from_buffer(buf),
                                POINTER(c_char))
                    length = X.utf8_lookup_string(ic, byref(x_key_press_event),
                                                buf_p, length, byref(keysym),
                                                byref(status))

                status = status.value
                keysym = keysym.value
                if status == X.LOOKUP_BOTH or status == X.LOOKUP_KEYSYM:
                    if keysym == X.K_Escape or keysym == X.K_Clear:
                        pwd = []
                        continue
                    elif keysym == X.K_Delete or keysym == X.K_BackSpace:
                        if pwd:
                            pwd.pop()
                        continue
                    elif keysym == X.K_LineFeed or keysym == X.K_Return:
                        if pw_verifier(b''.join(pwd)):
                            break
                        else:
                            pwd = []
                            if timeout:
                                goodwill += time_stamp - timeout
                                if goodwill > MAXGOODWILL:
                                    goodwill = MAXGOODWILL
                            timeout = -int(goodwill * GOODWILLPORTION)
                            goodwill += timeout
                            timeout += time_stamp + TIMEOUTPERATTEMPT
                            continue

                if status == X.LOOKUP_BOTH or status == X.LOOKUP_CHARS:
                    if length and sum(map(len, pwd)) < PWD_LENGTH_LIMIT:
                        pwd.append(bytes(buf[:length]))

    X.close_window(display)
