#!/usr/bin/python3

import logging
import struct
import sys

from lxml import etree

# .kvk file format
# KVK files are variable length files with variable sized structures.

# Magic   4 bytes 'KVKF'
# Version 4 bytes 0x600
# Flags   1 byte bitmask: [102key?, DisplayUnderlying?, UseUnderlying?, AltGr?]
kvkk102key = b'\x01'
kvkkDisplayUnderlying = b'\x02'
kvkkUseUnderlying = b'\x04'
kvkkAltGr = b'\x08'
# AssociatedKeyboard NSTRING
# AnsiFont NFONT
# UnicodeFont NFONT

# KeyCount: DWORD
# Keys: NKEY[KeyCount]


class KVKData:
    magic = ""
    version = None
    flags = 0
    key102 = False
    DisplayUnderlying = False
    UseUnderlying = False
    AltGr = False
    AssociatedKeyboard = ""
    AnsiFont = None
    UnicodeFont = None
    KeyCount = 0
    Keys = []


# NSTRING = (Length: Word; Chars: WChar[Length])
# NFONT = (Name: NSTRING; Size: DWORD; Color: DWORD (RGBQuad))

class NFont:
    name = ""
    size = 0
    red = 0
    green = 0
    blue = 0
    resv = 0


# NKEY = (
#   Flags: BYTE;   // 1:kvkkBitmap, 2:kvkkUnicode
kvkkBitmap = b'\x01'
kvkkUnicode = b'\x02'
#   Shift: WORD;   // See KVKS_* below
#   VKey: WORD;
#   Text: NSTRING;
#   Bitmap: NBITMAP
# )
# NBITMAP = (BitmapSize: DWORD; Bitmap: BYTE[BitmapSize])


class NKey:
    number = 0
    flags = 0
    hasBitmap = False
    hasUnicode = False
    shiftflags = 0
    Normal = False
    Shift = False
    Ctrl = False
    Alt = False
    LCtrl = False
    RCtrl = False
    LAlt = False
    RAlt = False
    VKey = 0
    text = ""
    bitmap = None


#   // Note that these differ from the KMX modifier bitmasks
#   KVKS_NORMAL =  0;
#   KVKS_SHIFT  =  1;
#   KVKS_CTRL   =  2;
#   KVKS_ALT    =  4;
#   KVKS_LCTRL  =  8;
#   KVKS_RCTRL  = 16;
#   KVKS_LALT   = 32;
#   KVKS_RALT   = 64;
KVKS_NORMAL = b'\x00'
KVKS_SHIFT = b'\x01'
KVKS_CTRL = b'\x02'
KVKS_ALT = b'\x04'
KVKS_LCTRL = b'\x08'
KVKS_RCTRL = b'\x10'
KVKS_LALT = b'\x20'
KVKS_RALT = b'\x40'


# from web/source/kmwosk.ts
VKey_to_Iso = {
    90: {"code": "B01", "base": "z", "shift": "Z"},   # Z
    88: {"code": "B02", "base": "x", "shift": "X"},   # X
    67: {"code": "B03", "base": "c", "shift": "C"},   # C
    86: {"code": "B04", "base": "v", "shift": "V"},   # V
    66: {"code": "B05", "base": "b", "shift": "B"},   # B
    78: {"code": "B06", "base": "n", "shift": "N"},   # N
    77: {"code": "B07", "base": "m", "shift": "M"},   # M
    188: {"code": "B08", "base": ",", "shift": "<"},  # ,
    190: {"code": "B09", "base": ".", "shift": ">"},  # .
    191: {"code": "B10", "base": "/", "shift": "?"},  # /
    65: {"code": "C01", "base": "a", "shift": "A"},   # A
    83: {"code": "C02", "base": "s", "shift": "S"},   # S
    68: {"code": "C03", "base": "d", "shift": "D"},   # D
    70: {"code": "C04", "base": "f", "shift": "F"},   # F
    71: {"code": "C05", "base": "g", "shift": "G"},   # G
    72: {"code": "C06", "base": "h", "shift": "H"},   # H
    74: {"code": "C07", "base": "j", "shift": "J"},   # J
    75: {"code": "C08", "base": "k", "shift": "K"},   # K
    76: {"code": "C09", "base": "l", "shift": "L"},   # L
    186: {"code": "C10", "base": ";", "shift": ":"},  # ;
    222: {"code": "C11", "base": "'", "shift": '"'},  # '
    81: {"code": "D01", "base": "q", "shift": "Q"},   # Q
    87: {"code": "D02", "base": "w", "shift": "W"},   # W
    69: {"code": "D03", "base": "e", "shift": "E"},   # E
    82: {"code": "D04", "base": "r", "shift": "R"},   # R
    84: {"code": "D05", "base": "t", "shift": "T"},   # T
    89: {"code": "D06", "base": "y", "shift": "Y"},   # Y
    85: {"code": "D07", "base": "u", "shift": "U"},   # U
    73: {"code": "D08", "base": "i", "shift": "I"},   # I
    79: {"code": "D09", "base": "o", "shift": "O"},   # O
    80: {"code": "D10", "base": "p", "shift": "P"},   # P
    219: {"code": "D11", "base": "[", "shift": "{"},  # [
    221: {"code": "D12", "base": "]", "shift": "}"},  # ]
    49: {"code": "E01", "base": "1", "shift": "!"},   # 1
    50: {"code": "E02", "base": "2", "shift": "@"},   # 2
    51: {"code": "E03", "base": "3", "shift": "#"},   # 3
    52: {"code": "E04", "base": "4", "shift": "$"},   # 4
    53: {"code": "E05", "base": "5", "shift": "%"},   # 5
    54: {"code": "E06", "base": "6", "shift": "^"},   # 6
    55: {"code": "E07", "base": "7", "shift": "&"},   # 7
    56: {"code": "E08", "base": "8", "shift": "*"},   # 8
    57: {"code": "E09", "base": "9", "shift": "("},   # 9
    48: {"code": "E10", "base": "0", "shift": ")"},   # 0
    189: {"code": "E11", "base": "-", "shift": "_"},  # -
    187: {"code": "E12", "base": "=", "shift": "+"},  # =
    192: {"code": "E00", "base": "`", "shift": "~"},  # `
    220: {"code": "C12", "base": "\\", "shift": "|"},  # \
    226: {"code": "B00", "base": "<", "shift": ">"},  # extra key on european keyboards
    32: {"code": "A03", "base": " ", "shift": " "},   # space
    97: {"code": "B51", "base": "1", "shift": "1"},   # "K_NP1"
    98: {"code": "B52", "base": "2", "shift": "2"},   # "K_NP2"
    99: {"code": "B53", "base": "3", "shift": "3"},   # "K_NP3"
    100: {"code": "C51", "base": "4", "shift": "4"},  # "K_NP4"
    101: {"code": "C52", "base": "5", "shift": "5"},  # "K_NP5"
    102: {"code": "C53", "base": "6", "shift": "6"},  # "K_NP6"
    103: {"code": "D51", "base": "7", "shift": "7"},  # "K_NP7"
    104: {"code": "D52", "base": "8", "shift": "8"},  # "K_NP8"
    105: {"code": "D53", "base": "9", "shift": "9"},  # "K_NP9"
}


def bytecheck(value, check):
    if bytes([value & check[0]]) == check:
        return True
    else:
        return False


def get_nkey(file, fileContent, offset):
    nkey = NKey()
    data = struct.unpack_from("<B2H", fileContent, offset)

    nkey.flags = data[0]
    nkey.hasBitmap = bytecheck(data[0], kvkkBitmap)
    nkey.hasUnicode = bytecheck(data[0], kvkkUnicode)

    nkey.shiftflags = data[1]
    if data[1] == 0:
        nkey.Normal = True
    nkey.Shift = bytecheck(data[1], KVKS_SHIFT)
    nkey.Ctrl = bytecheck(data[1], KVKS_CTRL)
    nkey.Alt = bytecheck(data[1], KVKS_ALT)
    nkey.LCtrl = bytecheck(data[1], KVKS_LCTRL)
    nkey.RCtrl = bytecheck(data[1], KVKS_RCTRL)
    nkey.LAlt = bytecheck(data[1], KVKS_LALT)
    nkey.RAlt = bytecheck(data[1], KVKS_RALT)

    nkey.VKey = data[2]

    nkey.text, newoffset = get_nstring(file, fileContent, offset + struct.calcsize("<B2H"))
    nkey.bitmap, newoffset = get_nbitmap(file, fileContent, newoffset)

    return nkey, newoffset


def get_nfont(file, fileContent, offset):
    nfont = NFont()
    nfont.name, curoffset = get_nstring(file, fileContent, offset)
    data = struct.unpack_from("<L4B", fileContent, curoffset)
    nfont.resv = data[4]
    nfont.blue = data[1]
    nfont.green = data[2]
    nfont.red = data[3]
    nfont.size = data[0]
    return nfont, curoffset + struct.calcsize("<L4B")


def get_nstring(file, fileContent, offset):
    stringlength = struct.unpack_from("<H", fileContent, offset)
    file.seek(offset + 2)
    if stringlength[0] > 256:
        logging.error("error: suspiciously long string. ABORT.")
        sys.exit(5)
    if stringlength[0]:
        # don't read the null string terminator
        stringdata = file.read((stringlength[0] - 1) * 2)
    else:
        stringdata = file.read(0)
    return stringdata.decode('utf-16'), offset + 2 + (2 * stringlength[0])


def get_nbitmap(file, fileContent, offset):
    bitmap = None
    bitmaplength = struct.unpack_from("<I", fileContent, offset)
    file.seek(offset + struct.calcsize("<I"))
    bitmap = file.read(bitmaplength[0])
    return bitmap, offset + struct.calcsize("<I") + bitmaplength[0]


def print_kvk(kvkData, allkeys=False):
    print("keyboard:", kvkData.AssociatedKeyboard)
    print("version", kvkData.version)
    print("flags:", kvkData.flags)
    if kvkData.key102:
        print("  keyboard has 102 keys?")
    if kvkData.DisplayUnderlying:
        print("  keyboard displays underlying?")
    if kvkData.UseUnderlying:
        print("  keyboard uses underlying?")
    if kvkData.AltGr:
        print("  keyboard uses AltGr?")

    print("AnsiFont:", kvkData.AnsiFont.name)
    print("  size:", kvkData.AnsiFont.size)
    print("  colour: r:%d g:%d b:%d a:%d" % (
        kvkData.AnsiFont.red, kvkData.AnsiFont.green, kvkData.AnsiFont.blue,
        kvkData.AnsiFont.resv))
    print("UnicodeFont:", kvkData.UnicodeFont.name)
    print("  size:", kvkData.UnicodeFont.size)
    print("  colour: r:%d g:%d b:%d a:%d" % (
        kvkData.UnicodeFont.red, kvkData.UnicodeFont.green, kvkData.UnicodeFont.blue,
        kvkData.UnicodeFont.resv))
    print("numkeys:", kvkData.KeyCount)
    if allkeys:
        for key in kvkData.Keys:
            print("number:", key.number)
            if key.hasBitmap:
                print("  key has bitmap")
            if key.hasUnicode:
                print("  key has unicode text")
            if key.Normal:
                print("  normal key")
            if key.Shift:
                print("  shift key")
            if key.Ctrl:
                print("  ctrl key")
            if key.Alt:
                print("  alt key")
            if key.LCtrl:
                print("  left ctrl key")
            if key.RCtrl:
                print("  right ctrl key")
            if key.LAlt:
                print("  left alt key")
            if key.RAlt:
                print("  right alt key")
            print("  vkey:", key.VKey)
            print("  text:", key.text)


def plus_join(modifier, name):
    if modifier:
        modifier = modifier + "+"
    modifier = modifier + name
    return modifier


def get_modifer(key):
    modifier = ""
    if key.Normal:
        return "None"
    if key.Shift:
        modifier = plus_join(modifier, "shift")
    if key.Ctrl:
        modifier = plus_join(modifier, "ctrl")
    if key.Alt:
        modifier = plus_join(modifier, "alt")
    if key.LCtrl:
        modifier = plus_join(modifier, "ctrlL")
    if key.RCtrl:
        modifier = plus_join(modifier, "ctrlR")
    if key.LAlt:
        modifier = plus_join(modifier, "altL")
    if key.RAlt:
        modifier = plus_join(modifier, "altR")
    return modifier


def convert_ldml(kvkData):
    keymaps = {}

    for key in kvkData.Keys:
        modifier = get_modifer(key)
        if modifier in keymaps:
            keymaps[modifier] = keymaps[modifier] + (key,)
        else:
            keymaps[modifier] = (key,)

    for vkey in VKey_to_Iso:
        alreadyused = False
        if "None" in keymaps:
            for key in keymaps["None"]:
                if key.VKey == vkey:
                    alreadyused = True
        if not alreadyused and vkey != 226:
            uskey = NKey()
            uskey.VKey = vkey
            uskey.text = VKey_to_Iso[vkey]["base"]
            if "None" in keymaps:
                keymaps["None"] = keymaps["None"] + (uskey,)
            else:
                keymaps["None"] = (uskey,)
        alreadyused = False
        if "shift" in keymaps:
            for key in keymaps["shift"]:
                if key.VKey == vkey:
                    alreadyused = True
        if not alreadyused and vkey != 226:
            uskey = NKey()
            uskey.VKey = vkey
            uskey.text = VKey_to_Iso[vkey]["shift"]
            if "shift" in keymaps:
                keymaps["shift"] = keymaps["shift"] + (uskey,)
            else:
                keymaps["shift"] = (uskey,)

    ldml = etree.Element("keyboard", locale="zzz-keyman")
    etree.SubElement(ldml, "version", platform="11")
    names = etree.SubElement(ldml, "names")
    names.append(etree.Element("name", value="ZZZ"))

    for modifier in keymaps:
        if modifier == "None":
            keymap = etree.SubElement(ldml, "keyMap")
        else:
            keymap = etree.SubElement(ldml, "keyMap", modifiers=modifier)
        for key in keymaps[modifier]:
            if key.VKey in VKey_to_Iso:
                iso_key = VKey_to_Iso[key.VKey]["code"]
                keymap.append(etree.Element("map", iso=iso_key, to=key.text))
            else:
                logging.warning("Unknown vkey: %s", key.VKey)
    return ldml


def output_ldml(ldmlfile, ldml):
    etree.ElementTree(ldml).write(ldmlfile, pretty_print=True)


def parse_kvk_file(kvkfile):
    kvkData = KVKData()
    try:
        with open(kvkfile, mode='rb') as file:  # b is important -> binary
            fileContent = file.read()

            kvkstart = struct.unpack_from("<4s4cc", fileContent, 0)
            kvkData.version = (kvkstart[1], kvkstart[2], kvkstart[3], kvkstart[4])
            kvkData.flags = kvkstart[5]
            kvkData.key102 = bytecheck(kvkData.flags[0], kvkk102key)
            kvkData.DisplayUnderlying = bytecheck(kvkData.flags[0], kvkkDisplayUnderlying)
            kvkData.UseUnderlying = bytecheck(kvkData.flags[0], kvkkUseUnderlying)
            kvkData.AltGr = bytecheck(kvkData.flags[0], kvkkAltGr)

            kvkData.AssociatedKeyboard, newoffset = get_nstring(file, fileContent, struct.calcsize("<4s4cc"))
            kvkData.AnsiFont, newoffset = get_nfont(file, fileContent, newoffset)
            kvkData.UnicodeFont, newoffset = get_nfont(file, fileContent, newoffset)
            numkeys = struct.unpack_from("I", fileContent, newoffset)
            kvkData.KeyCount = numkeys[0]
            newoffset = newoffset + struct.calcsize("I")

            for num in range(numkeys[0]):
                nkey, newoffset = get_nkey(file, fileContent, newoffset)
                nkey.number = num
                kvkData.Keys.append(nkey)
    except Exception as e:
        logging.warning('Exception %s parsing kvk file %s %s', type(e), kvkfile, e.args)
    return kvkData


def convert_kvk_to_ldml(kvkfile):
    kvkData = parse_kvk_file(kvkfile)
    return convert_ldml(kvkData)
