#!/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 datetime
import infi.systray
import json
import keyboard
import Pinyin2Hanzi
import threading
import time
import tkinter
import traceback
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

# == 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(screencaret_x, screencaret_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" % (screencaret_x, screencaret_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()

# Pinyin2Hanzi
# https://github.com/letiantian/Pinyin2Hanzi
def convert_pinyin_to_hanzi(source):
    hmmparams = Pinyin2Hanzi.DefaultHmmParams()
    quotation = "\""
    leftbracket = "["
    rightbracket = "]"
    source = source.strip()
    source = source.replace(" ", "\", \"")
    source = leftbracket + quotation + source + quotation + rightbracket
    source = json.loads(source)
    try:
        response = Pinyin2Hanzi.viterbi(hmm_params=hmmparams,
                                  observations=source, path_num=5)
    except:
        traceback.print_exc()
        response = ["error"]
    finally:
        pass
    result = []
    if response == ["error"]:
        result = response
    else:
        for item in response:
            result.append(item.path)
        result = str(result)
        result = result.replace("\', \'", "")
        result = result.replace("[\'", "\"")
        result = result.replace("\']", "\"")
        result = json.loads(result)
    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, " ===== OPTIONS ===== ")
    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_backspace():
    keyboard.send("backspace")

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

def sleep_1ms():
    time.sleep(0.001)

def sleep_5cs():
    time.sleep(0.05)

# infi.systray
# https://github.com/Infinidat/infi.systray
def define_systray():
    global systray
    systray = infi.systray.SysTrayIcon("icon-zh.ico", "now converting")

def start_systray():
    systray.start()

def shutdown_systray():
    systray.shutdown()

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

print("hi")
define_systray()
start_systray()

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

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()

    screencaret_x, screencaret_y = get_coordinates()
    thread1 = get_source(screencaret_x, screencaret_y)
    threading_start(thread1, "sourcethread", "sourcethread")
    if end == True:
        break

    if len(source) > 0:
        optionlist = convert_pinyin_to_hanzi(source)

    if len(source) > 0:
        if not optionlist == ["error"]:
            thread2 = select_word_from_list(optionlist,
                                            screencaret_x, screencaret_y)
            threading_start(thread2, "resultthread", "resultthread")
    else:
        converted = ""
    if end == True:
        break

    set_foreground_window(fghWnd)
    
    if len(converted) > 0:
        write_word(converted)
    else:
        if not optionlist == ["error"]:
            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")