#!/usr/bin/python3

import argparse
import logging
import os.path
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):
    plus = "+"
    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
        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
        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()
    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)
    return kvkData

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