import sys
import os
import array
import math
import tkinter as tk
from tkinter import messagebox, filedialog
import numpy as np
import cv2
import datetime
import json

GRID_SIZE = 100
CELL_SIZE = 6
PADDING = 8

class TkFontEditor:
    def __init__(self, root):
        self.root = root
        self.root.title("和文私用領域フォントエディタ (100x100 保存ボタン完全版)")
        self.root.resizable(False, False)

        self.grid = [[False for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.strokes = []
        self.current_stroke = []

        self.total_size = GRID_SIZE * CELL_SIZE

        # 左側：キャンバス領域
        left_frame = tk.Frame(root)
        left_frame.pack(side="left", padx=15, pady=15)

        self.canvas = tk.Canvas(left_frame, width=self.total_size, height=self.total_size, bg="white", highlightthickness=0)
        self.canvas.pack()

        # 右側：操作パネル領域
        right_frame = tk.Frame(root, width=280)
        right_frame.pack(side="right", fill="y", padx=15, pady=15)

        guide_label = tk.Label(right_frame, text="【 マウス操作 】", font=("MS Gothic", 9, "bold"), anchor="w")
        guide_label.pack(fill="x", pady=(0, 2))

        guides = [
            "・左ドラッグ : ドットを自由描画",
            "・Ctrl + 左クリック : 塗りつぶし",
            "・右ドラッグ : ドットを消去",
            "・Ctrl + S   : フォント＆HTML出力",
            "・Ctrl + O   : 現在のドット絵を保存",
            "・Ctrl + R   : 保存したドット絵を読込"
        ]
        for g in guides:
            tk.Label(right_frame, text=g, font=("MS Gothic", 9), anchor="w", fg="#333333").pack(fill="x")

        # Unicode設定
        unicode_title = tk.Label(right_frame, text="\n割り当てるUnicodeアドレス (16進数 4桁):", font=("MS Gothic", 10), justify="left", anchor="w")
        unicode_title.pack(fill="x")

        u_input_frame = tk.Frame(right_frame)
        u_input_frame.pack(fill="x", pady=5)

        tk.Label(u_input_frame, text="U+", font=("Courier New", 11, "bold")).pack(side="left")
        self.unicode_entry = tk.Entry(u_input_frame, width=8, font=("Courier New", 11, "bold"), justify="center")
        self.unicode_entry.insert(0, "E004")
        self.unicode_entry.pack(side="left", padx=5)

        tk.Frame(right_frame, height=2, bd=1, relief="sunken").pack(fill="x", pady=15)

        # ─── 保存・読み込みボタン（確実に表示される位置へ配置） ───
        save_load_frame = tk.Frame(right_frame)
        save_load_frame.pack(fill="x", pady=5)

        self.btn_save = tk.Button(save_load_frame, text="ドット絵保存\n(Ctrl+O)", command=self.save_grid_data, font=("MS Gothic", 9))
        self.btn_save.pack(side="left", fill="x", expand=True, padx=(0, 2))

        self.btn_load = tk.Button(save_load_frame, text="ドット絵読込\n(Ctrl+R)", command=self.load_grid_data, font=("MS Gothic", 9))
        self.btn_load.pack(side="right", fill="x", expand=True, padx=(2, 0))

        self.btn_clear = tk.Button(right_frame, text="全クリア", command=self.clear_canvas, height=1)
        self.btn_clear.pack(fill="x", pady=3)

        self.btn_undo = tk.Button(right_frame, text="1画戻す (Ctrl+Z)", command=self.undo_last_stroke, height=1)
        self.btn_undo.pack(fill="x", pady=3)

        self.btn_submit = tk.Button(right_frame, text="フォント＆HTML出力\n(Ctrl+S)", command=self.build_ttf, bg="#e1f5fe", fg="#0277bd", font=("MS Gothic", 9, "bold"), height=2)
        self.btn_submit.pack(fill="x", pady=(15, 0))

        self.canvas.bind("<Button-1>", self.on_left_click)
        self.canvas.bind("<B1-Motion>", self.on_left_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_left_release)
        self.canvas.bind("<B3-Motion>", self.on_right_drag)
        self.canvas.bind("<Button-3>", self.on_right_drag)

        self.root.bind("<Control-z>", lambda e: self.undo_last_stroke())
        self.root.bind("<Control-Z>", lambda e: self.undo_last_stroke())
        self.root.bind("<Control-s>", lambda e: self.build_ttf_from_shortcut())
        self.root.bind("<Control-S>", lambda e: self.build_ttf_from_shortcut())
        self.root.bind("<Control-o>", lambda e: self.save_grid_data())
        self.root.bind("<Control-O>", lambda e: self.save_grid_data())
        self.root.bind("<Control-r>", lambda e: self.load_grid_data())
        self.root.bind("<Control-R>", lambda e: self.load_grid_data())

        self.draw_ui_base()
        self.canvas.focus_set()

    def draw_ui_base(self):
        self.canvas.delete("all")
        for y in range(GRID_SIZE):
            for x in range(GRID_SIZE):
                x1, y1 = x * CELL_SIZE, y * CELL_SIZE
                x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
                bg_color = "#e6f0ff" if (x < PADDING or x >= (GRID_SIZE - PADDING) or y < PADDING or y >= (GRID_SIZE - PADDING)) else "white"
                line_color = "#cbdcfa" if (x < PADDING or x >= (GRID_SIZE - PADDING) or y < PADDING or y >= (GRID_SIZE - PADDING)) else "#e6e6e6"
                if self.grid[y][x]:
                    bg_color = "#282828"
                    self.canvas.create_rectangle(x1, y1, x2, y2, fill=bg_color, outline=bg_color, tags=f"cell_{x}_{y}")
                else:
                    self.canvas.create_rectangle(x1, y1, x2, y2, fill=bg_color, outline=line_color, tags=f"cell_{x}_{y}")

    def on_left_click(self, event):
        if event.state & 0x0004:
            x, y = event.x // CELL_SIZE, event.y // CELL_SIZE
            if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
                self.flood_fill(x, y)
        else:
            x, y = event.x // CELL_SIZE, event.y // CELL_SIZE
            if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
                self.grid[y][x] = True
                self.current_stroke.append((x, y))
                self.canvas.itemconfig(f"cell_{x}_{y}", fill="#ff9999", outline="#ff9999")

    def on_left_drag(self, event):
        if event.state & 0x0004: return
        x, y = event.x // CELL_SIZE, event.y // CELL_SIZE
        if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
            if self.current_stroke and self.current_stroke[-1] == (x, y): return
            self.grid[y][x] = True
            self.current_stroke.append((x, y))
            self.canvas.itemconfig(f"cell_{x}_{y}", fill="#ff9999", outline="#ff9999")

    def on_left_release(self, event):
        if self.current_stroke:
            self.strokes.append(list(self.current_stroke))
        self.current_stroke = []
        self.draw_ui_base()

    def flood_fill(self, start_x, start_y):
        target_color = self.grid[start_y][start_x]
        if target_color == True: return

        filled_pts = []
        queue = [(start_x, start_y)]
        self.grid[start_y][start_x] = True
        filled_pts.append((start_x, start_y))

        while queue:
            cx, cy = queue.pop(0)
            for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
                nx, ny = cx + dx, cy + dy
                if 0 <= nx < GRID_SIZE and 0 <= ny < GRID_SIZE:
                    if self.grid[ny][nx] == False:
                        self.grid[ny][nx] = True
                        filled_pts.append((nx, ny))
                        queue.append((nx, ny))

        if filled_pts:
            self.strokes.append(filled_pts)
        self.draw_ui_base()

    def on_right_drag(self, event):
        x, y = event.x // CELL_SIZE, event.y // CELL_SIZE
        if 0 <= x < GRID_SIZE and 0 <= y < GRID_SIZE:
            self.grid[y][x] = False
            for s in self.strokes:
                if (x, y) in s: s.remove((x, y))
            self.strokes = [s for s in self.strokes if len(s) > 0]
            self.draw_ui_base()

    # ─── 先生のランレングス圧縮（RLE）を取り入れたデータ保存メソッド ───
    def save_grid_data(self):
        import re
        hex_str = self.unicode_entry.get().strip().upper()
        desktop_dir = os.path.join(os.path.expanduser("~"), "Desktop")
        default_filename = f"GridData_RLE_{hex_str}.json"

        file_path = filedialog.asksaveasfilename(
            initialdir=desktop_dir,
            initialfile=default_filename,
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json")]
        )
        if not file_path: return

        # 1. 【エリア符号化】 2x2の4マスを1つの16進数文字に変換 (100x100 -> 50x50の文字列へ)
        raw_hex_string = ""
        for y in range(0, GRID_SIZE, 2):
            for x in range(0, GRID_SIZE, 2):
                # 2x2マスの状態をビット(1/0)として取得
                b1 = 1 if self.grid[y][x] else 0
                b2 = 1 if self.grid[y][x+1] else 0
                b3 = 1 if self.grid[y+1][x+1] else 0
                b4 = 1 if self.grid[y+1][x] else 0

                # 当時のVBAのSelect Caseをビット演算で完璧に再現
                # (1,1,1,1=15=F, 0,0,0,0=0)
                val = (b1 << 3) | (b2 << 2) | (b3 << 1) | b4
                raw_hex_string += f"{val:X}"  # 16進数(0-F)の大文字文字列にする

        # 2. 【ランレングス圧縮】 当時のVBAの正規表現パターンをPythonで再現！
        # 0が5回以上連続する箇所などを「文字{回数}」に置換。今回は文字数制限を撤去し限界まで縮めます
        pattern = re.compile(r"([0-9A-F])\1{4,}")
        compressed_string = pattern.sub(lambda m: f"{m.group(1)}{{{len(m.group(0))}}}", raw_hex_string)

        save_data = {
            "rle_grid_string": compressed_string,
            "strokes": self.strokes
        }
        try:
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(save_data, f, indent=4)
            messagebox.showinfo("成功", f"【圧縮成功】\n元の文字数: {len(raw_hex_string)}文字\n圧縮後: {len(compressed_string)}文字\n\n先生のRLE方式で保存しました！")
        except Exception as e:
            messagebox.showerror("エラー", f"保存に失敗しました:\n{e}")

    # ─── 圧縮された特殊文字列を解凍して復元するデータ読み込みメソッド ───
    def load_grid_data(self):
        import re
        desktop_dir = os.path.join(os.path.expanduser("~"), "Desktop")
        file_path = filedialog.askopenfilename(
            initialdir=desktop_dir,
            filetypes=[("JSON Files", "*.json")]
        )
        if not file_path: return

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                load_data = json.load(f)

            # もし古い形式（前回の1と0の配列）のファイルだった場合は、自動で互換処理を行う
            if "grid" in load_data:
                self.grid = [[bool(cell) for cell in row] for row in load_data["grid"]]
            else:
                # ─── 先生のRLE文字列を解凍するロジック ───
                compressed_string = load_data["rle_grid_string"]

                # 「A{12}」のような記述を「AAAAAAAAAAAA」に正規表現で展開（デコード）
                expand_pattern = re.compile(r"([0-9A-F])\{(\d+)\}")
                decompressed_string = expand_pattern.sub(lambda m: m.group(1) * int(m.group(2)), compressed_string)

                # 16進数文字列から 100x100 の二次元配列(True/False)に復元
                self.grid = [[False for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
                idx = 0
                for y in range(0, GRID_SIZE, 2):
                    for x in range(0, GRID_SIZE, 2):
                        val = int(decompressed_string[idx], 16)
                        # ビットを分解して2x2マスに書き戻す
                        self.grid[y][x]     = bool(val & 8)
                        self.grid[y][x+1]   = bool(val & 4)
                        self.grid[y+1][x+1] = bool(val & 2)
                        self.grid[y+1][x]   = bool(val & 1)
                        idx += 1

            self.strokes = []
            for stroke in load_data["strokes"]:
                self.strokes.append([tuple(pt) for pt in stroke])

            self.draw_ui_base()
            messagebox.showinfo("成功", "先生のRLEデータを解凍・復元しました！")
        except Exception as e:
            messagebox.showerror("エラー", f"読み込みに失敗しました:\n{e}")


    def build_ttf_from_shortcut(self):
        self.root.focus_set()
        self.build_ttf()

    def build_ttf(self):
        from fontTools.fontBuilder import FontBuilder
        from fontTools.ttLib.tables._g_l_y_f import Glyph, GlyphCoordinates
        from fontTools.ttLib.tables import ttProgram

        hex_str = self.unicode_entry.get().strip().upper()
        try:
            target_unicode = int(hex_str, 16)
        except ValueError:
            messagebox.showerror("エラー", "有効な16進数4桁を入力してください。")
            return

        if not any(any(row) for row in self.grid):
            messagebox.showwarning("エラー", "線が描かれていません。")
            return

        # 1. 二値画像化 (※ここのインデントを修正しました)
        img_np = np.zeros((GRID_SIZE, GRID_SIZE), dtype=np.uint8)
        for y in range(GRID_SIZE):
            for x in range(GRID_SIZE):
                if self.grid[y][x]:
                    img_np[y][x] = 255

        # 2. 輪郭抽出（中抜き対応モード）
        contours, _ = cv2.findContours(img_np, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

        final_coords = []
        end_points = []
        scale = 1000 / GRID_SIZE

        for cnt in contours:
            epsilon = 1.0
            approx = cv2.approxPolyDP(cnt, epsilon, True)
            if len(approx) < 3: continue

            contour_pts = []
            for pt in approx:
                gx, gy = pt[0][0], pt[0][1]
                font_x = int((gx + 0.5) * scale)
                font_y = int(1000 - (gy + 0.5) * scale)
                contour_pts.append((font_x, font_y))

            final_coords.extend(contour_pts)
            end_points.append(len(final_coords) - 1)

        if not final_coords:
            messagebox.showwarning("エラー", "有効な輪郭が抽出できませんでした。")
            return

        now_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        dynamic_font_name = f"MyGridFont_{hex_str}_{now_str}"
        font_filename = f"MyRawFont_{hex_str}_{now_str}.ttf"
        html_filename = f"Test_{hex_str}_{now_str}.html"
        dynamic_glyph_name = f"glyph_{hex_str}"

        g = Glyph()
        g.numberOfContours = len(end_points)
        g.endPtsOfContours = end_points
        g.coordinates = GlyphCoordinates(final_coords)
        g.flags = array.array('B', [1] * len(final_coords))
        g.program = ttProgram.Program()

        notdef_g = Glyph()
        notdef_g.numberOfContours = 0
        notdef_g.endPtsOfContours = []
        notdef_g.coordinates = GlyphCoordinates([])
        notdef_g.flags = array.array('B', [])
        notdef_g.program = ttProgram.Program()

        fb = FontBuilder(1000, isTTF=True)
        fb.setupGlyphOrder(['.notdef', dynamic_glyph_name])
        fb.setupGlyf({'.notdef': notdef_g, dynamic_glyph_name: g})
        fb.setupCharacterMap({target_unicode: dynamic_glyph_name})
        fb.setupHorizontalMetrics({'.notdef': (1000, 0), dynamic_glyph_name: (1000, 0)})
        fb.setupHorizontalHeader(ascent=800, descent=-200)

        nameStrings = {
            'familyName': dynamic_font_name,
            'styleName': 'Regular',
            'uniqueFontIdentifier': f'1.000;FontTools;{dynamic_font_name}',
            'fullName': dynamic_font_name,
            'version': 'Version 1.000',
            'psName': f'{dynamic_font_name}-Regular',
        }
        fb.setupNameTable(nameStrings)
        fb.setupOS2(sTypoAscender=800, sTypoDescender=-200)
        fb.setupHead(fontRevision=1.0)
        fb.setupMaxp()
        fb.setupPost()

        desktop_dir = os.path.join(os.path.expanduser("~"), "Desktop")
        ttf_path = os.path.join(desktop_dir, font_filename)
        html_path = os.path.join(desktop_dir, html_filename)

        fb.font.recalcBBox = True
        fb.font.recalcTimestamp = True

        try:
            fb.font.save(ttf_path)

            html_content = f"""<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>高解像度平滑化フォントテスト</title>
    <style>
        @font-face {{
            font-family: '{dynamic_font_name}';
            src: url('{font_filename}') format('truetype');
        }}
        .char {{
            font-family: '{dynamic_font_name}', sans-serif;
            font-size: 160px;
            color: #111;
            background: #fafafa;
            border: 1px dashed #bbb;
            display: inline-block;
            padding: 20px;
        }}
    </style>
</head>
<body>
    <h1>高解像度平滑化テスト (U+{hex_str})</h1>
    <div class="char">&#x{hex_str};</div>
</body>
</html>"""
            with open(html_path, "w", encoding="utf-8") as f:
                f.write(html_content)

            messagebox.showinfo("成功", f"フォント・HTML出力成功！\n\n・{font_filename}\n・{html_filename}")
        except Exception as e:
            messagebox.showerror("書き込みエラー", f"失敗しました:\n{e}")

    def undo_last_stroke(self):
        if self.strokes:
            for x, y in self.strokes.pop():
                self.grid[y][x] = False
            self.draw_ui_base()

    def clear_canvas(self):
        self.grid = [[False for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.strokes = []
        self.current_stroke = []
        self.draw_ui_base()

if __name__ == "__main__":
    root = tk.Tk()
    app = TkFontEditor(root)
    root.mainloop()
