#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#Copyright 2018 Sodium "natoriusushio" Chloride
#
#Released under the MIT license
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"),
#to deal in the Software without restriction, including without limitation
#the rights to use, copy, modify, merge, publish, distribute, sublicense,
#and/or sell copies of the Software, and to permit persons to whom
#the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
#TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
#OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#https://opensource.org/licenses/mit-license.php
#http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license

import ctypes
import datetime
import infi.systray
import json
import keyboard
import romkan
import socket
import threading
import time
import tkinter
import win32api
import win32gui
import win32process

# ==== definition area ====
# == variable ==
abort = False
autophagy = False
backspaceisreleased = False
destroy = False
end = False
firstloop = True

chkfghWnd = 0

boxname = ""
converted = ""
chkfgWndt = ""
source = ""

systray = None

# == exception handling ==
exception = [",", "|", ";", " "]
comma = ["、", "，", ","]
semicolon = ["；", ";"]
space = ["　", " "]
verticalbar =["｜", "￤", "∥", "|", "¦", "‖"]

bracket = ["[", "]", "(", ")"]

# == win32api ==
# pywin32
# https://github.com/mhammond/pywin32
# http://timgolden.me.uk/pywin32-docs/index.html
def attach_thread_input(current, target, boolean):
    win32process.AttachThreadInput(current, target, boolean)

def client_to_screen(target, client_x, client_y):
    coordinates = (client_x, client_y)
    screen_x, screen_y = win32gui.ClientToScreen(target, coordinates)
    return screen_x, screen_y

def find_window(classname, windowname):
    result = win32gui.FindWindow(classname, windowname)
    return result

def get_caret_pos():
    result = win32gui.GetCaretPos()
    return result

def get_current_thread_id():
    result = win32api.GetCurrentThreadId()
    return result

def get_foreground_window():
    result = win32gui.GetForegroundWindow()
    return result

def get_system_metrics():
    screen_x = win32api.GetSystemMetrics(0)
    screen_y = win32api.GetSystemMetrics(1)
    return screen_x, screen_y

def get_window_rect(hWnd):
    topleft_x, topleft_y, bottomright_x, bottomright_y \
    = win32gui.GetWindowRect(hWnd)
    return topleft_x, topleft_y, bottomright_x, bottomright_y

def get_window_text(hWnd):
    result = win32gui.GetWindowText(hWnd)
    return result

def get_window_thread_process_id(hWnd):
    targetthreadid, targetprocessid = win32process.GetWindowThreadProcessId(
                                                                        hWnd)
    return targetthreadid, targetprocessid

def set_foreground_window(hWnd):
    win32gui.SetForegroundWindow(hWnd)

# == function ==
def apoptosis(boolean):
    global destroy
    destroy = boolean
    global end
    end = boolean
    global autophagy
    autophagy = boolean

# keyboard
# https://github.com/boppreh/keyboard
def terminator():
    keyboard.add_hotkey("ctrl+alt+del", apoptosis, args=[True])

def backspace_is_released(boolean):
    global backspaceisreleased
    backspaceisreleased = boolean

def add_hotkey_backspace_is_released():
    keyboard.add_hotkey("backspace", backspace_is_released,
                        args=[True], trigger_on_release=True)

def get_convwindow_size(): # convwindow = window for converting
    topleft_x =0
    topleft_y =0
    bottomright_x =144
    bottomright_y =21
    return bottomright_x, bottomright_y

# I referred to these topics for writing the function below.
# https://stackoverflow.com/questions/19724360/python-get-caret-position
# http://timgolden.me.uk/python/win32_how_do_i/find-the-screen-resolution.html
def get_coordinates():
        targetwindow = get_foreground_window()
        targetthreadid, targetprocessid = get_window_thread_process_id(
                                                                targetwindow)
        currentthreadid = get_current_thread_id()
        try:
            attach_thread_input(currentthreadid, targetthreadid, True)
            clientcaret_x, clientcaret_y = get_caret_pos()
        finally:
            attach_thread_input(currentthreadid, targetthreadid, False)
        if (clientcaret_x, clientcaret_y) == (None, None):
            screen_x, screen_y = get_system_metrics()
            convwin_x, convwin_y = get_convwindow_size()
            screencaret_x = (screen_x - convwin_x) // 2
            screencaret_y = 2 * (screen_y - convwin_y) // 3
        else:
            if (clientcaret_x, clientcaret_y) == (0, 0):
                # topleft=tpl, bottomright=btmr
                tpl_x, tpl_y, btmr_x, btmr_y = get_window_rect(targetwindow)
                convwin_x, convwin_y = get_convwindow_size()
                tmp_x = (btmr_x - tpl_x - convwin_x) // 2
                tmp_y = 2 * (btmr_y - tpl_y - convwin_x) // 3
                screencaret_x = tpl_x + tmp_x
                screencaret_y = tpl_y + tmp_y
            else:
                screencaret_x, screencaret_y = client_to_screen(
                                                        targetwindow,
                                                        clientcaret_x,
                                                        clientcaret_y)
        return screencaret_x, screencaret_y

def get_source(caretpositionglobal_x, caretpositionglobal_y):
    global destroy
    destroy = False
    label = "conv"
    identifier = datetime.datetime.now().strftime("%f")
    global boxname
    boxname = label+identifier
    root = tkinter.Tk()
    root.title(boxname)
    root.attributes("-topmost", True)
    root.overrideredirect(1)
    coordinate = "+%s+%s" % (caretpositionglobal_x, caretpositionglobal_y)
    root.geometry(coordinate)
    getentry = tkinter.Entry()
    getentry.config(background="azure")
    def get_entry(event):
        keyboard.release("enter")
        global source
        source = getentry.get()
        global destroy
        destroy = True
    def terminate(event):
        keyboard.release("\\")
        keyboard.release("ctrl")
        global end
        end = True
        fgw = get_foreground_window()
        fgwt = get_window_text(fgw)
        global destroy
        destroy = True
    getentry.pack()
    getentry.focus_set()
    root.bind("<Return>", get_entry)
    root.bind("<Control-\\>", terminate)
    while destroy == False:
        time.sleep(0.001) # for reducing CPU usage
        root.update()
        fgw = get_foreground_window()
        fgwt = get_window_text(fgw)
        if fgwt == boxname:
            pass
        else:
            target = find_window("TkTopLevel", boxname)
            set_foreground_window(target)
    destroy = False
    getentry.destroy()
    root.destroy()

# romkan
# https://github.com/soimort/python-romkan
# I referred to this topic for writing the function below.
# https://masatoi.github.io/2017/11/19/skk-client
def convert_romaji_to_kanji(source):
    skkreq = "1"
    skksource = ""
    skkspacer = " "
    skkhost = "127.0.0.1"
    skkport = 1178
    skkencoding = "euc_jp"
    hiraganasource = romkan.to_hiragana(source)
    katakanasource = romkan.to_katakana(source)
    skksource = skkreq + hiraganasource + skkspacer
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((skkhost, skkport))
    client.send(skksource.encode(skkencoding))
    result = client.recv(1024)
    client.shutdown(socket.SHUT_RDWR)
    client.close()
    if result.decode(skkencoding) == "4\n":
        result = []
    else:
        result = result.decode(skkencoding)
        result = result.replace("1/", "[\"")
        result = result.replace("/\n", "\"]")
        result = result.replace("/", "\", \"")
        result = json.loads(result)
    if hiraganasource not in result:
        try:
            result.insert(0, hiraganasource)
        except:
            print("不正な文字列です")
    if katakanasource not in result:
        try:
            result.append(katakanasource)
        except:
            print("不正な文字列です")
    if source not in result:
        try:
            result.append(source)
        except:
            print("不正な文字列です")
    if source in bracket:
        for en, ja in zip(["(", ")", "[", "]"], ["「", "」", "「", "」"]):
            if source is en:
                if ja not in result:
                    result.append(ja)
    return result

def select_word_from_list(result, screencaret_x, screencaret_y):
    global destroy
    destroy = False
    counts = len(result)
    label = "conv"
    identifier = datetime.datetime.now().strftime("%f")
    global boxname
    boxname = label+identifier
    root = tkinter.Tk()
    root.title(boxname)
    root.attributes("-topmost", True)
    root.overrideredirect(1)
    coordinates = "+%s+%s" % (screencaret_x, screencaret_y)
    root.geometry(coordinates)
    selected = tkinter.StringVar()
    selected.set("")
    entry = tkinter.Entry(root, textvariable = selected)
    entry.pack()
    listbox = tkinter.Listbox(root)
    listbox.pack()
    listbox.focus_set()
    listbox.insert(tkinter.END, " ===== 変換候補 ===== ")
    for item in result:
        listbox.insert(tkinter.END, item)
    selected.set(listbox.get(1))
    def select_next(event):
        keyboard.release("space")
        nowselected = listbox.index("active")
        if nowselected < counts:
            listbox.see(nowselected+1)
            listbox.activate(nowselected+1)
            selected.set(listbox.get(nowselected+1))
        else:
            listbox.activate(1)
            listbox.see(1)
            selected.set(listbox.get(1))
    def select_prev(event):
        keyboard.release("space")
        keyboard.release("shift")
        nowselected = listbox.index("active")
        if nowselected > 1:
            listbox.see(nowselected-1)
            listbox.activate(nowselected-1)
            selected.set(listbox.get(nowselected-1))
        else:
            listbox.activate(counts)
            listbox.see(counts)
            selected.set(listbox.get(counts))
    def get_entry(event):
        keyboard.release("enter")
        nowselected = listbox.index("active")
        global converted
        if nowselected > 0:
            converted = listbox.get("active")
        else:
            converted = listbox.get(1)
        global destroy
        destroy = True
    def terminate(event):
        keyboard.release("\\")
        keyboard.release("ctrl")
        global end
        end = True
        global destroy
        destroy = True
    listbox.bind("<space>", select_next)
    listbox.bind("<Shift-space>", select_prev)
    listbox.bind("<Return>", get_entry)
    listbox.bind("<Control-\\>", terminate)
    while destroy == False:
        time.sleep(0.001) # for reducing CPU usage
        root.update()
        fgw = get_foreground_window()
        fgwt = get_window_text(fgw)
        if fgwt == boxname:
            pass
        else:
            target = find_window("TkTopLevel", boxname)
            set_foreground_window(target)
    destroy = False
    listbox.destroy()
    root.destroy()

def threading_start(threaddef, threadname, targetname):
    threadname = threading.Thread(target=threaddef, name=targetname)
    threadname.start()

def write_word(word):
    time.sleep(0.001)
    keyboard.write(word)

def release_keys():
    keyboard.release("backspace")
    keyboard.release("space")
    keyboard.release("shift")
    keyboard.release("ctrl")
    keyboard.release("enter")

def unhook_all():
    keyboard.unhook_all()

#def send_key(key):
#    keyboard.send(key)

def send_backspace():
    keyboard.send("backspace")

def send_enter():
    keyboard.send("enter")

def send_plus():
    ctypes.windll.user32.keybd_event(0x6B, 0, 0, 0)
    ctypes.windll.user32.keybd_event(0x6B, 0, 0x0002, 0)

def sleep_1ms():
    time.sleep(0.001)

def sleep_5cs():
    time.sleep(0.05)

def replace_verticalbar(source):
    result = source.replace("|", "｜")
    return result

def define_systray():
    global systray
    systray = infi.systray.SysTrayIcon("icon.ico", "変換中")

def start_systray():
    systray.start()

def shutdown_systray():
    systray.shutdown()

# ==== algorithm area ====
# something like pseudocode oriented programming style, "pops" ;-)

print("hi")

#print("Select window, then press shift.") # for testing
#keyboard.wait("shift") # for testing

define_systray()
start_systray()

#chkfghWnd = get_foreground_window() # check foreground handle of window
#chkfgWndt= get_window_text(chkfghWnd) # check foreground window text
if "conv" in chkfgWndt:
    print("Another process is already running!")
    print("Program will be aborted.")
    end = True
    abort = True

while end == False:
    fghWnd = 0 # foreground handle of window
    screencaret_x = 0
    screencaret_y = 0

    backspaceisreleased = False
    split = False
    thread1 = None
    thread2 = None

    source = ""
    converted = ""

    optionlist = []

    release_keys()
    unhook_all()
    add_hotkey_backspace_is_released()

# for avoiding a confliction between this program
# and the ctrl+alt+del security option window
    terminator()

    if firstloop == False:
        sleep_1ms()
    fghWnd = get_foreground_window()
    sleep_1ms()
    screencaret_x, screencaret_y = get_coordinates()
    sleep_1ms()
    thread1 = get_source(screencaret_x, screencaret_y)
    sleep_1ms()
    threading_start(thread1, "sourcethread", "sourcethread")
    if end == True:
        break
    sleep_1ms()
    set_foreground_window(fghWnd)

    quotation = "\""
    leftbracket = "["
    rightbracket = "]"
    sourcegen = source.strip()
    sourcegen = sourcegen.replace(" ", "\", \"")
    sourcegen = leftbracket + quotation + sourcegen + quotation + rightbracket
    sourcegen = json.loads(sourcegen)
    print(str(sourcegen))

    sourcenew = []
    for item in sourcegen:
        if item is not "":
            sourcenew.append(item)
    sourcenum = len(sourcenew)
    print(str(sourcenew))

    sourcecounter = 0
    while sourcecounter < sourcenum:
        sourcecounter = sourcecounter + 1
    
    sourcecounter = 0
    while sourcecounter < sourcenum:
        sleep_1ms()
        set_foreground_window(fghWnd)
        if source is not " ":
            source = sourcenew[sourcecounter]

        if len(source) == 1:
            if source in exception:
                for src, alt in zip([",", "|", ";"],
                                    [comma, verticalbar, semicolon]):
                    if source is src:
                        optionlist = alt
            else:
                optionlist = convert_romaji_to_kanji(source)
        else:
            if "|" in source:
                source = replace_verticalbar(source)
            optionlist = convert_romaji_to_kanji(source)
        sleep_1ms()
        set_foreground_window(fghWnd)
        sleep_1ms()
        screencaret_x, screencaret_y = get_coordinates()
        sleep_1ms()
        thread2 = select_word_from_list(optionlist,
                                        screencaret_x, screencaret_y)
        sleep_1ms()
        threading_start(thread2, "resultthread", "resultthread")
        if end == True:
            break
        sleep_1ms()
        set_foreground_window(fghWnd)

        if converted is not ";":
            split = False
            while split == False:
                if ";" in converted: # for SKKDict with annotation
                    converted = converted[:-1]
                else:
                    split = True
        if converted is not "+":
            write_word(converted)
        else: # because keyboard.send("+") replies ";" on ROS's japanese env
            send_plus()
        sourcecounter = sourcecounter + 1

    if source is " ":
        optionlist = space
        sleep_1ms()
        thread3 = select_word_from_list(optionlist,
                                        screencaret_x, screencaret_y)
        sleep_1ms()
        threading_start(thread3, "resultthread", "resultthread")
        sleep_1ms()
        set_foreground_window(fghWnd)
        sleep_1ms()
        write_word(converted)

    if len(converted) == 0:
        if backspaceisreleased == True:
            send_backspace()
        else:
            send_enter()
    firstloop = False

if abort == False:
    set_foreground_window(fghWnd)
release_keys()
unhook_all()
sleep_5cs()
shutdown_systray()
sleep_5cs()
if autophagy == False:
    print("bye")
else:
    print("adieu")
