﻿#!/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 urllib
import urllib.request
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 ==
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

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()

# 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.replace("|", "｜")

def define_systray():
    global systray
    systray = infi.systray.SysTrayIcon("icon.ico", "変換中")

def start_systray():
    systray.start()

def shutdown_systray():
    systray.shutdown()

# ==== algorithm area ====
# pseudocode oriented programming style, "pops" ;-)

print("hi")
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()

    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:
        if len(source) == 1:
            if source in exception:
                for src, alt in zip([",", "|", ";", " "],
                                    [comma, verticalbar, semicolon, space]):
                    if source is src:
                        optionlist = alt
            else:
                optionlist = convert_romaji_to_kanji(source)
        else:
            if "|" in source:
                source = replace_verticalbar()
            optionlist = convert_romaji_to_kanji(source)

    if len(source) > 0:
        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:
        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 japanese environment
            send_plus()
    else:
        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")
