import pygame
import random
import time
import json
import os
from typing import Dict, List

pygame.init()

# 屏幕与帧率
SCREEN_WIDTH = 1000
SCREEN_HEIGHT = 700
FPS = 60

# 颜色
WHITE = (255, 255, 255)
BLACK = (20, 20, 20)
GRAY = (140, 140, 140)
LIGHT_GRAY = (230, 230, 230)
DARK_GRAY = (100, 100, 100)
GREEN = (34, 177, 76)
RED = (200, 30, 30)
BLUE = (0, 120, 215)
YELLOW = (255, 200, 0)
PURPLE = (160, 100, 200)

# 文件名
STATS_FILE = "typing_stats.json"


class TypingTrainer:
    def __init__(self):
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        pygame.display.set_caption("高级打字练习软件")
        self.clock = pygame.time.Clock()

        # 字体
        self.font = pygame.font.SysFont(None, 30)
        self.large_font = pygame.font.SysFont(None, 48)
        self.mono_font = pygame.font.SysFont("Consolas,monospace", 28)

        # 状态
        self.state = "menu"  # menu, typing, results, settings, stats
        self.difficulty = "medium"  # easy, medium, hard
        self.text_length = 80

        # 文本库（可扩展或改为从文件加载）
        self.text_library = {
            "easy": [
                "the quick brown fox jumps over the lazy dog",
                "hello world this is a simple typing test",
                "practice makes perfect in typing skills",
                "typing is an essential skill for everyone"
            ],
            "medium": [
                "The quick brown fox jumps over the lazy dog. This sentence contains all letters of the alphabet.",
                "Programming requires patience and consistent practice to master effectively.",
                "Learning to type quickly and accurately will improve your productivity significantly.",
                "Modern technology has made communication faster and more accessible than ever before."
            ],
            "hard": [
                "Supercalifragilisticexpialidocious! Even though the sound of it is something quite atrocious.",
                "The juxtaposition of disparate elements creates an unexpected yet harmonious composition.",
                "Phenomenological investigations into consciousness reveal intricate neural correlates.",
                "Cryptocurrency blockchain technology utilizes cryptographic hash functions for security."
            ]
        }

        # 练习会话变量
        self.current_text = ""
        self.user_input = ""
        self.is_typing = False
        self.start_time = 0.0
        self.end_time = 0.0
        self.wpm = 0
        self.accuracy = 100.0
        self.errors = 0

        # 统计数据
        self.stats = self.load_stats()

        # UI 矩形
        self.text_box = pygame.Rect(50, 150, 900, 200)
        self.input_box = pygame.Rect(50, 380, 900, 60)

    def load_stats(self) -> Dict:
        try:
            if os.path.exists(STATS_FILE):
                with open(STATS_FILE, "r", encoding="utf-8") as f:
                    return json.load(f)
        except Exception:
            pass
        return {"sessions": [], "best_wpm": 0}

    def save_stats(self):
        try:
            with open(STATS_FILE, "w", encoding="utf-8") as f:
                json.dump(self.stats, f, ensure_ascii=False, indent=2)
        except Exception:
            pass

    def get_random_text(self) -> str:
        texts = self.text_library.get(
            self.difficulty, self.text_library["medium"])
        text = random.choice(texts)
        # 拼接直到满足长度
        while len(text) < self.text_length:
            text += " " + random.choice(texts)
        return text[:self.text_length]

    def start_session(self):
        self.current_text = self.get_random_text()
        self.user_input = ""
        self.is_typing = True
        self.start_time = time.time()
        self.end_time = 0.0
        self.wpm = 0
        self.accuracy = 100.0
        self.errors = 0

    def end_session(self):
        self.is_typing = False
        self.end_time = time.time()
        duration = max(0.001, self.end_time - self.start_time)
        words_typed = len(self.user_input) / 5.0
        self.wpm = int((words_typed / duration) * 60)
        self.accuracy = self.calculate_accuracy()
        # 存储会话
        session = {
            "timestamp": time.time(),
            "difficulty": self.difficulty,
            "text_length": len(self.current_text),
            "wpm": self.wpm,
            "accuracy": round(self.accuracy, 1),
            "duration": round(duration, 2)
        }
        self.stats["sessions"].append(session)
        if self.wpm > self.stats.get("best_wpm", 0):
            self.stats["best_wpm"] = self.wpm
        self.save_stats()
        self.state = "results"

    def calculate_accuracy(self) -> float:
        if len(self.user_input) == 0:
            return 100.0
        correct = 0
        for i, ch in enumerate(self.user_input):
            if i < len(self.current_text) and ch == self.current_text[i]:
                correct += 1
        return (correct / len(self.user_input)) * 100.0

    def calculate_realtime_metrics(self):
        if not self.is_typing:
            return
        elapsed = max(0.001, time.time() - self.start_time)
        words_typed = len(self.user_input) / 5.0
        self.wpm = int((words_typed / elapsed) * 60)
        self.accuracy = self.calculate_accuracy()

    def handle_keydown_typing(self, event):
        if event.key == pygame.K_BACKSPACE:
            if len(self.user_input) > 0:
                self.user_input = self.user_input[:-1]
        elif event.key == pygame.K_RETURN or event.key == pygame.K_KP_ENTER:
            # 如果用户已完成或按回车结束
            if len(self.user_input) >= len(self.current_text):
                self.end_session()
            else:
                # 允许回车在文本中作为字符（可视情况开启）
                pass
        elif event.key == pygame.K_ESCAPE:
            # 取消当前练习返回菜单
            self.state = "menu"
            self.is_typing = False
        else:
            # 只接受可显示字符
            char = event.unicode
            if char:
                # 过滤不可打印字符（例如 ctrl 等）
                if len(char) == 1 and ord(char) >= 32:
                    # 如果输入长度超过目标长度，一般我们允许继续但不计算正确率后的字符
                    self.user_input += char

    def draw_menu(self):
        self.screen.fill(WHITE)
        title = self.large_font.render("高级打字练习软件", True, BLUE)
        self.screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 40))

        # 按钮
        buttons = [
            ("开始练习", (SCREEN_WIDTH//2 - 120, 150, 240, 60), "start"),
            ("设置", (SCREEN_WIDTH//2 - 120, 230, 240, 60), "settings"),
            ("统计", (SCREEN_WIDTH//2 - 120, 310, 240, 60), "stats"),
            ("退出", (SCREEN_WIDTH//2 - 120, 390, 240, 60), "quit")
        ]
        mouse = pygame.mouse.get_pos()
        click = pygame.mouse.get_pressed()

        for text, rect, action in buttons:
            r = pygame.Rect(rect)
            color = BLUE if r.collidepoint(mouse) else DARK_GRAY
            pygame.draw.rect(self.screen, color, r, border_radius=8)
            label = self.font.render(text, True, WHITE)
            self.screen.blit(
                label, (r.centerx - label.get_width()//2, r.centery - label.get_height()//2))
            if r.collidepoint(mouse) and click[0] == 1:
                # 简单防止持续触发
                pygame.time.delay(150)
                if action == "start":
                    self.start_session()
                    self.state = "typing"
                elif action == "settings":
                    self.state = "settings"
                elif action == "stats":
                    self.state = "stats"
                elif action == "quit":
                    pygame.quit()
                    raise SystemExit

        # 显示最佳成绩
        best = self.stats.get("best_wpm", 0)
        best_text = self.font.render(f"历史最佳: {best} WPM", True, BLACK)
        self.screen.blit(best_text, (SCREEN_WIDTH//2 -
                         best_text.get_width()//2, 480))

        hint = self.font.render("按空格或点击开始来开始练习。Esc 在练习中返回菜单。", True, DARK_GRAY)
        self.screen.blit(hint, (SCREEN_WIDTH//2 - hint.get_width()//2, 520))

    def draw_typing(self):
        self.screen.fill(WHITE)
        # 上方信息
        info = f"难度: {self.difficulty.capitalize()}    目标长度: {len(self.current_text)}"
        info_surf = self.font.render(info, True, DARK_GRAY)
        self.screen.blit(info_surf, (50, 20))

        # 返回按钮
        back = pygame.Rect(50, 60, 80, 34)
        pygame.draw.rect(self.screen, LIGHT_GRAY, back, border_radius=6)
        pygame.draw.rect(self.screen, DARK_GRAY, back, 2, border_radius=6)
        back_label = self.font.render("返回", True, BLACK)
        self.screen.blit(back_label, (back.centerx - back_label.get_width() //
                         2, back.centery - back_label.get_height()//2))

        mouse = pygame.mouse.get_pos()
        click = pygame.mouse.get_pressed()
        if back.collidepoint(mouse) and click[0] == 1:
            pygame.time.delay(150)
            self.state = "menu"
            self.is_typing = False

        # 文本区
        pygame.draw.rect(self.screen, LIGHT_GRAY,
                         self.text_box, border_radius=8)
        pygame.draw.rect(self.screen, DARK_GRAY,
                         self.text_box, 2, border_radius=8)

        # 绘制目标文本（分行并高亮）
        padding = 12
        x = self.text_box.x + padding
        y = self.text_box.y + padding
        max_width = self.text_box.width - padding*2

        # 使用单字符绘制以实现逐字符高亮
        for i, ch in enumerate(self.current_text):
            # 计算是否换行
            ch_surf = self.mono_font.render(ch, True, BLACK)
            if x + ch_surf.get_width() > self.text_box.x + max_width:
                x = self.text_box.x + padding
                y += ch_surf.get_height() + 4

            # 决定颜色：已输入且正确=绿，已输入但错误=红，未输入=黑
            color = BLACK
            if i < len(self.user_input):
                if self.user_input[i] == ch:
                    color = GREEN
                else:
                    color = RED
            ch_surf = self.mono_font.render(ch, True, color)
            self.screen.blit(ch_surf, (x, y))
            x += ch_surf.get_width()

        # 输入框
        pygame.draw.rect(self.screen, WHITE, self.input_box, border_radius=8)
        pygame.draw.rect(self.screen, DARK_GRAY,
                         self.input_box, 2, border_radius=8)

        # 闪烁光标与输入显示
        display_text = self.user_input
        if self.is_typing:
            if (pygame.time.get_ticks() // 500) % 2 == 0:
                display_text += "|"
        input_surf = self.mono_font.render(display_text, True, BLACK)
        self.screen.blit(
            input_surf, (self.input_box.x + 12, self.input_box.y + 12))

        # 实时统计
        self.calculate_realtime_metrics()
        stats = f"WPM: {self.wpm}    准确率: {self.accuracy:.1f}%    输入长度: {len(self.user_input)}/{len(self.current_text)}"
        stats_surf = self.font.render(stats, True, BLACK)
        self.screen.blit(stats_surf, (50, self.input_box.y +
                         self.input_box.height + 12))

        completion_hint = self.font.render(
            "完成后按回车查看结果，或继续输入覆盖目标之外的字符。", True, DARK_GRAY)
        self.screen.blit(
            completion_hint, (50, self.input_box.y + self.input_box.height + 44))

    def draw_results(self):
        self.screen.fill(WHITE)
        title = self.large_font.render("练习结果", True, BLUE)
        self.screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 40))

        # 显示详细统计
        duration = max(0.001, self.end_time - self.start_time) if self.end_time else max(
            0.001, time.time() - self.start_time)
        lines = [
            f"WPM: {self.wpm}",
            f"准确率: {self.accuracy:.1f}%",
            f"用时: {duration:.2f} 秒",
            f"文本长度: {len(self.current_text)} 字符",
            f"难度: {self.difficulty.capitalize()}",
            f"历史最佳: {self.stats.get('best_wpm', 0)} WPM"
        ]
        y = 140
        for line in lines:
            surf = self.font.render(line, True, BLACK)
            self.screen.blit(surf, (SCREEN_WIDTH//2 - surf.get_width()//2, y))
            y += 40

        # 按钮：再试一次 / 菜单
        retry = pygame.Rect(SCREEN_WIDTH//2 - 140, 420, 120, 50)
        menu_btn = pygame.Rect(SCREEN_WIDTH//2 + 20, 420, 120, 50)
        mx, my = pygame.mouse.get_pos()
        click = pygame.mouse.get_pressed()

        for r, text in [(retry, "再试一次"), (menu_btn, "返回菜单")]:
            color = BLUE if r.collidepoint((mx, my)) else DARK_GRAY
            pygame.draw.rect(self.screen, color, r, border_radius=8)
            lbl = self.font.render(text, True, WHITE)
            self.screen.blit(lbl, (r.centerx - lbl.get_width() //
                             2, r.centery - lbl.get_height()//2))
            if r.collidepoint((mx, my)) and click[0] == 1:
                pygame.time.delay(150)
                if text == "再试一次":
                    self.start_session()
                    self.state = "typing"
                else:
                    self.state = "menu"

    def draw_settings(self):
        self.screen.fill(WHITE)
        title = self.large_font.render("设置", True, BLUE)
        self.screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 40))

        # 难度选择
        label = self.font.render("难度:", True, BLACK)
        self.screen.blit(label, (120, 150))
        diffs = [("easy", "简单"), ("medium", "中等"), ("hard", "困难")]
        mx, my = pygame.mouse.get_pos()
        click = pygame.mouse.get_pressed()

        for i, (key, name) in enumerate(diffs):
            r = pygame.Rect(220 + i*160, 140, 140, 44)
            color = YELLOW if self.difficulty == key else LIGHT_GRAY
            if r.collidepoint((mx, my)):
                color = (min(color[0]+20, 255), color[1], color[2])
            pygame.draw.rect(self.screen, color, r, border_radius=8)
            pygame.draw.rect(self.screen, DARK_GRAY, r, 2, border_radius=8)
            lbl = self.font.render(name, True, BLACK)
            self.screen.blit(lbl, (r.centerx - lbl.get_width() //
                             2, r.centery - lbl.get_height()//2))
            if r.collidepoint((mx, my)) and click[0] == 1:
                pygame.time.delay(120)
                self.difficulty = key

        # 文本长度选择
        label2 = self.font.render("目标长度:", True, BLACK)
        self.screen.blit(label2, (120, 220))
        lengths = [30, 50, 80, 120]
        for i, L in enumerate(lengths):
            r = pygame.Rect(220 + i*140, 210, 120, 40)
            color = LIGHT_GRAY if self.text_length != L else YELLOW
            if r.collidepoint((mx, my)):
                color = (min(color[0]+20, 255), color[1], color[2])
            pygame.draw.rect(self.screen, color, r, border_radius=8)
            pygame.draw.rect(self.screen, DARK_GRAY, r, 2, border_radius=8)
            lbl = self.font.render(f"{L} chars", True, BLACK)
            self.screen.blit(lbl, (r.centerx - lbl.get_width() //
                             2, r.centery - lbl.get_height()//2))
            if r.collidepoint((mx, my)) and click[0] == 1:
                pygame.time.delay(120)
                self.text_length = L

        # 返回按钮
        back = pygame.Rect(50, 60, 80, 34)
        pygame.draw.rect(self.screen, LIGHT_GRAY, back, border_radius=6)
        pygame.draw.rect(self.screen, DARK_GRAY, back, 2, border_radius=6)
        back_label = self.font.render("返回", True, BLACK)
        self.screen.blit(back_label, (back.centerx - back_label.get_width() //
                         2, back.centery - back_label.get_height()//2))
        if back.collidepoint((mx, my)) and click[0] == 1:
            pygame.time.delay(120)
            self.state = "menu"

    def draw_stats(self):
        self.screen.fill(WHITE)
        title = self.large_font.render("历史统计", True, BLUE)
        self.screen.blit(title, (SCREEN_WIDTH//2 - title.get_width()//2, 30))

        # 最近若干场会话
        y = 110
        sessions = list(reversed(self.stats.get("sessions", [])))[:10]
        if not sessions:
            hint = self.font.render("暂无练习记录。", True, DARK_GRAY)
            self.screen.blit(
                hint, (SCREEN_WIDTH//2 - hint.get_width()//2, 150))
        else:
            for s in sessions:
                t = time.localtime(s["timestamp"])
                ts = time.strftime("%Y-%m-%d %H:%M", t)
                line = f"{ts} | 难度:{s['difficulty']} | WPM:{s['wpm']} | 准确率:{s['accuracy']}% | 用时:{s['duration']}s"
                surf = self.font.render(line, True, BLACK)
                self.screen.blit(surf, (60, y))
                y += 34
                if y > SCREEN_HEIGHT - 80:
                    break

        # 返回按钮
        back = pygame.Rect(50, 60, 80, 34)
        pygame.draw.rect(self.screen, LIGHT_GRAY, back, border_radius=6)
        pygame.draw.rect(self.screen, DARK_GRAY, back, 2, border_radius=6)
        back_label = self.font.render("返回", True, BLACK)
        self.screen.blit(back_label, (back.centerx - back_label.get_width() //
                         2, back.centery - back_label.get_height()//2))
        mx, my = pygame.mouse.get_pos()
        click = pygame.mouse.get_pressed()
        if back.collidepoint((mx, my)) and click[0] == 1:
            pygame.time.delay(120)
            self.state = "menu"

    def run(self):
        running = True
        while running:
            dt = self.clock.tick(FPS)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

                elif event.type == pygame.KEYDOWN:
                    if self.state == "typing":
                        self.handle_keydown_typing(event)
                    else:
                        # 在菜单/设置界面也允许按空格开始
                        if event.key == pygame.K_SPACE and self.state == "menu":
                            self.start_session()
                            self.state = "typing"

                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        # 处理菜单中的按钮点击由 draw_* 时检查鼠标按下实现
                        # 这里只处理 typing 界面的输入框点击可以聚焦（当前实现不区分焦点）
                        pass

            # 绘制当前界面
            if self.state == "menu":
                self.draw_menu()
            elif self.state == "typing":
                self.draw_typing()
            elif self.state == "results":
                self.draw_results()
            elif self.state == "settings":
                self.draw_settings()
            elif self.state == "stats":
                self.draw_stats()

            pygame.display.flip()

        pygame.quit()


if __name__ == "__main__":
    app = TypingTrainer()
    app.run()
