#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
渲染层 —— 将数据绘制成 400x300 纯黑白墨水屏仪表盘。

设计要点：
  - 纯黑白（设备无真灰阶，灰阶只能抖动成网点，弃用）；用构图 + 字号 + 留白建立层级。
  - Fusion Pixel 点阵字体，按整数倍尺寸(12/24/48)渲染，fontmode='1' 关闭抗锯齿 → 像素锐利。
  - 重点数据用「伪粗体」(偏移叠绘)加粗，仍对齐像素网格。
布局：上=大时钟+日期 / 天气；中=橘喵今日经营三列；下=近30天三指标趋势折线图。
"""
import math
from PIL import Image, ImageDraw, ImageFont

import config

BLACK, WHITE = 0, 255


class DashboardRenderer:
    def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT)):
        self.W, self.H = size
        self.F12 = ImageFont.truetype(font_path, 12)
        self.F24 = ImageFont.truetype(font_path, 24)
        self.F48 = ImageFont.truetype(font_path, 48)

    # ---------- 基础绘制工具 ----------
    def _tw(self, s, ft):
        return self.d.textlength(s, font=ft)

    def _draw(self, x, y, s, ft, fill, bold):
        # 伪粗体：点阵字体单一字重，按 1~bold px 偏移叠绘加粗笔画
        self.d.text((x, y), s, font=ft, fill=fill)
        for dx in range(1, bold + 1):
            self.d.text((x + dx, y), s, font=ft, fill=fill)

    def text(self, x, y, s, ft, fill=BLACK, bold=0):
        self._draw(x, y, s, ft, fill, bold)

    def rtext(self, xr, y, s, ft, fill=BLACK, bold=0):
        self._draw(xr - self._tw(s, ft) - bold, y, s, ft, fill, bold)

    def ctext(self, cx, y, s, ft, fill=BLACK, bold=0):
        self._draw(cx - (self._tw(s, ft) + bold) / 2, y, s, ft, fill, bold)

    def hdash(self, x0, x1, y, dash=4, gap=4, fill=BLACK):
        x = x0
        while x < x1:
            self.d.line([x, y, min(x + dash, x1), y], fill=fill, width=1)
            x += dash + gap

    def vdash(self, x, y0, y1, dash=4, gap=4, fill=BLACK):
        y = y0
        while y < y1:
            self.d.line([x, y, x, min(y + dash, y1)], fill=fill, width=1)
            y += dash + gap

    def tri(self, cx, cy, sz, up=True, fill=BLACK):
        if up:
            pts = [(cx, cy - sz / 2), (cx - sz / 2, cy + sz / 2), (cx + sz / 2, cy + sz / 2)]
        else:
            pts = [(cx, cy + sz / 2), (cx - sz / 2, cy - sz / 2), (cx + sz / 2, cy - sz / 2)]
        self.d.polygon(pts, fill=fill)

    # ---------- 组件 ----------
    def _cmp_line(self, cx, y, label, pair, fg=BLACK):
        """环比/同比一行：标签 + 涨跌三角 + 数值，整体居中。direction 为 None 时不画三角。"""
        direction, val = pair
        lw = self._tw(label, self.F12); gap = 5; nw = self._tw(val, self.F12)
        trw, tgap = (8, 4) if direction else (0, 0)
        x = cx - (lw + gap + trw + tgap + nw) / 2
        self.text(x, y, label, self.F12, fill=fg); x += lw + gap
        if direction:
            # 三角中心对齐 F12 数字墨迹竖直中心(y+8.5)，否则箭头偏高
            self.tri(x + trw / 2, y + 8.5, 7, up=(direction == "up"), fill=fg); x += trw + tgap
        self.text(x, y, val, self.F12, fill=fg)

    def _title_bar(self, y, title, upd):
        """标题栏：■ 标题（左） + 上次更新 HH:mm（右）。"""
        sq, sq_y = 6, y + 4
        self.d.rectangle([10, sq_y, 10 + sq, sq_y + sq], fill=BLACK)
        self.text(10 + sq + 6, y, title, self.F12)
        self.rtext(self.W - 10, y, f"上次更新 {upd}", self.F12)

    def _loc_pin(self, x, y, w=8, fg=BLACK, bg=WHITE):
        """定位图标：实心水滴(圆头+尖尾) + 白色挖孔，1-bit 下轮廓清晰。
        (x,y) 为头部圆左上角，w 为头径，总高约 w*1.5。"""
        cx, cy = x + w / 2, y + w / 2  # 头部圆心
        self.d.ellipse([x, y, x + w, y + w], fill=fg)                       # 头部实心圆
        self.d.polygon([(x, cy), (x + w, cy), (cx, y + w + w * 0.5)], fill=fg)  # 尾部尖
        hr = w * 0.2
        self.d.ellipse([cx - hr, cy - hr, cx + hr, cy + hr], fill=bg)       # 头部白孔

    @staticmethod
    def _weather_category(icon):
        """和风天气 icon 代码 → 可枚举类别。无法识别时回退 'cloudy'。
        参考 https://dev.qweather.com/docs/resource/icons/ ：
        100=晴日 150=晴夜 / 101~103,151~153=多云 / 104,154=阴 /
        302~304=雷阵雨 / 300~399=雨 / 400~499=雪 / 500~599=雾霾沙尘。"""
        try:
            c = int(icon)
        except (TypeError, ValueError):
            return "cloudy"
        if c == 100:
            return "sunny"
        if c == 150:
            return "clear_night"
        if c in (101, 102, 103, 151, 152, 153):
            return "cloudy"
        if c in (104, 154):
            return "overcast"
        if c in (302, 303, 304):
            return "thunder"
        if 300 <= c <= 399:
            return "rain"
        if 400 <= c <= 499:
            return "snow"
        if 500 <= c <= 599:
            return "fog"
        return "cloudy"

    def _sun(self, cx, cy, r, s, fg, rays=True):
        """太阳：圆 + 8 道光芒（光芒可关）。"""
        S = lambda v: v * s
        self.d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=fg, width=2)
        if rays:
            for a in range(0, 360, 45):
                rad = math.radians(a)
                self.d.line([cx + math.cos(rad) * (r + S(3)), cy + math.sin(rad) * (r + S(3)),
                             cx + math.cos(rad) * (r + S(7)), cy + math.sin(rad) * (r + S(7))], fill=fg, width=2)

    def _cloud(self, x, cy, s, fg, bg):
        """一朵云，cy 为云体中线高度；bg 填充内部以盖住其后的太阳光芒。"""
        S = lambda v: v * s
        self.d.ellipse([x + S(4), cy - S(2), x + S(20), cy + S(12)], fill=bg)
        self.d.ellipse([x + S(14), cy - S(8), x + S(34), cy + S(12)], fill=bg)
        self.d.rectangle([x + S(6), cy + S(4), x + S(34), cy + S(12)], fill=bg)
        self.d.arc([x + S(2), cy - S(4), x + S(22), cy + S(14)], 90, 270, fill=fg, width=2)
        self.d.arc([x + S(12), cy - S(10), x + S(36), cy + S(14)], 270, 90, fill=fg, width=2)
        self.d.line([x + S(9), cy + S(13), x + S(29), cy + S(13)], fill=fg, width=2)

    def _weather_icon(self, x, y, category, s=1.0, fg=BLACK, bg=WHITE):
        """按天气类别画图标，s 为缩放系数，原生约 36x38（左上角 x,y）。"""
        S = lambda v: v * s

        if category == "sunny":
            self._sun(x + S(18), y + S(19), S(11), s, fg)
            return

        if category == "clear_night":  # 月牙：实心圆挖去偏上右的背景圆
            mx, my, r = x + S(18), y + S(19), S(13)
            self.d.ellipse([mx - r, my - r, mx + r, my + r], fill=fg)
            ox, oy, r2 = mx + S(6), my - S(5), S(12)
            self.d.ellipse([ox - r2, oy - r2, ox + r2, oy + r2], fill=bg)
            return

        if category == "overcast":  # 阴：单独一朵云，居中偏下
            self._cloud(x, y + S(20), s, fg, bg)
            return

        if category == "fog":  # 雾/霾/沙尘：横向雾线，长短交错
            for i, dy in enumerate((9, 17, 25, 33)):
                inset = S(5) if i % 2 else 0
                self.d.line([x + inset, y + S(dy), x + S(34) - inset, y + S(dy)], fill=fg, width=2)
            return

        if category != "rain" and category != "snow" and category != "thunder":  # 多云及未识别：太阳 + 云
            self._sun(x + S(10), y + S(9), S(6), s, fg)
            self._cloud(x, y + S(24), s, fg, bg)
            return

        # 以下三类：云在上，降水/天气符号在云下方
        cb = y + S(15) + S(13)  # 云底 y
        self._cloud(x, y + S(15), s, fg, bg)
        if category == "rain":
            for dx in (9, 18, 27):
                self.d.line([x + S(dx) + S(3), cb + S(3), x + S(dx) - S(1), cb + S(10)], fill=fg, width=2)
        elif category == "snow":
            for dx in (10, 19, 28):
                fx, fy, rr = x + S(dx), cb + S(7), S(4)
                for a in (0, 60, 120):
                    rad = math.radians(a)
                    self.d.line([fx - math.cos(rad) * rr, fy - math.sin(rad) * rr,
                                 fx + math.cos(rad) * rr, fy + math.sin(rad) * rr], fill=fg, width=1)
        elif category == "thunder":
            bx = x + S(18)
            self.d.polygon([(bx - S(1), cb + S(2)), (bx + S(5), cb + S(2)), (bx + S(1), cb + S(8)),
                            (bx + S(6), cb + S(8)), (bx - S(4), cb + S(18)), (bx, cb + S(9)),
                            (bx - S(4), cb + S(9))], fill=fg)

    # 线型样式：(实线段长, 间隙长)；实线用 dash=0 表示连续
    _STYLES = {"solid": (0, 0), "dashed": (7, 4), "dotted": (1, 2)}

    def _styled_polyline(self, pts, style, fill=BLACK):
        """按线型(solid/dashed/dotted)沿折线绘制。点线/虚线靠等距步进采样。"""
        dash, gap = self._STYLES.get(style, (0, 0))
        if dash == 0:  # 实线：直接连线
            for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
                self.d.line([x0, y0, x1, y1], fill=fill, width=1)
            return
        period, drawn = dash + gap, 0.0  # drawn: 当前所在周期内已走过的距离
        for (x0, y0), (x1, y1) in zip(pts, pts[1:]):
            seg = math.hypot(x1 - x0, y1 - y0)
            if seg == 0:
                continue
            ux, uy = (x1 - x0) / seg, (y1 - y0) / seg
            t = 0.0
            while t < seg:
                phase = drawn % period
                if phase < dash:  # 处于“实”阶段 → 画一小段
                    step = min(dash - phase, seg - t)
                    sx, sy = x0 + ux * t, y0 + uy * t
                    ex, ey = x0 + ux * (t + step), y0 + uy * (t + step)
                    if dash <= 1:  # 点线：画方点更醒目
                        self.d.point((round(sx), round(sy)), fill=fill)
                    else:
                        self.d.line([sx, sy, ex, ey], fill=fill, width=1)
                    t += step; drawn += step
                else:            # 处于“虚”阶段 → 跳过
                    step = min(period - phase, seg - t)
                    t += step; drawn += step

    def _style_sample(self, x, y, style, w=22, fill=BLACK):
        """图例用的线型样例（一小段水平线）。"""
        self._styled_polyline([(x, y), (x + w, y)], style, fill=fill)

    def _trend_chart(self, y0, trend):
        """近30天三指标折线图：标题(左) + 紧凑图例(右) + 折线 + X轴(仅月/日)。
        各指标按自身极值独立归一化，故只表达趋势形状，不可横向比绝对值。"""
        dates, series = trend["dates"], trend["series"]
        leg_y = y0
        # ---- 标题（左）：■ 近30天趋势 ----
        sq, sq_y = 6, leg_y + 4
        self.d.rectangle([10, sq_y, 10 + sq, sq_y + sq], fill=BLACK)
        self.text(10 + sq + 6, leg_y, "橘喵·30天趋势", self.F12)
        # 无数据：画占位符 "--"，跳过图例/折线/X轴
        if not series or not dates or not any(s.get("data") for s in series):
            self.ctext(self.W / 2, leg_y + 36, "--", self.F24)
            return
        # ---- 图例（右）：紧凑、整体右对齐，从右往左逐项排布 ----
        SAMP, G1, GAP = 16, 8, 10  # 样例线长 / 线与名间隙 / 图例项间隙
        x = self.W - 10
        for s in reversed(series):
            self.rtext(x, leg_y, s["k"], self.F12)        # 指标名右端对齐 x
            x -= self._tw(s["k"], self.F12) + G1
            self._style_sample(x - SAMP, leg_y + 7, s["style"])
            x -= SAMP + GAP
        # ---- 绘图区 ----
        px0, px1 = 14, self.W - 10
        py0, py1 = leg_y + 22, leg_y + 22 + 56  # 顶/底
        self.hdash(px0, px1, py1, dash=2, gap=3)  # 基线
        n = len(dates)
        xstep = (px1 - px0) / (n - 1) if n > 1 else 0
        for s in series:
            data = s["data"]
            lo, hi = min(data), max(data)
            span = (hi - lo) or 1
            pad = 4  # 上下留白，避免贴边
            pts = []
            for i, v in enumerate(data):
                x = px0 + xstep * i
                yv = py1 - pad - (v - lo) / span * (py1 - py0 - 2 * pad)
                pts.append((x, yv))
            self._styled_polyline(pts, s["style"])
        # ---- X 轴标签：仅首 / 中 / 末三个日期 ----
        xl_y = py1 + 3
        self.text(px0, xl_y, dates[0], self.F12)
        self.ctext((px0 + px1) / 2, xl_y, dates[n // 2], self.F12)
        self.rtext(px1, xl_y, dates[-1], self.F12)

    # ---------- 入口 ----------
    def render(self, data):
        """根据 data 渲染并返回 PIL.Image（'L' 模式，纯 0/255）。"""
        self.img = Image.new("L", (self.W, self.H), WHITE)
        self.d = ImageDraw.Draw(self.img)
        self.d.fontmode = "1"  # 关闭抗锯齿，点阵锐利

        dt, wt, mao = data["date"], data["weather"], data["mao"]

        # A. Hero：大时钟 + 日期（时钟上移使「顶部留白 ≈ 与日期间距」对称）
        self.text(10, 0, dt["time"], self.F48, bold=2)
        self.text(12, 64, f'{dt["week"]}   {dt["greg"]}', self.F12)

        # B. 天气：右对齐文字 + 居中的 28° + 放大右靠的图标
        DIV_Y = 90  # 天气区底部分割线
        loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]}  {wt["range"]}'
        self.rtext(390, 8, loc, self.F12)
        self.rtext(390, 30, temp, self.F24, bold=1)
        self.rtext(390, 64, cr, self.F12)
        # 定位图标：置于 loc 文字左侧，留间距并与文字竖直居中
        PIN_W, PIN_GAP = 8, 6
        loc_pin_x = 390 - self._tw(loc, self.F12) - PIN_GAP - PIN_W
        self._loc_pin(loc_pin_x, 10, PIN_W)
        s = 1.5
        iw, ih = 36 * s, 38 * s  # 图标原生约 36x38
        text_left = min(loc_pin_x,
                        390 - self._tw(temp, self.F24) - 1,
                        390 - self._tw(cr, self.F12))
        icon_y = (DIV_Y - ih) / 2  # 顶部(0)与分割线正中：上下留白相等
        self._weather_icon(text_left - 12 - iw, icon_y, self._weather_category(wt.get("icon")), s=s)

        self.hdash(8, 392, DIV_Y)

        # C. 橘喵今日经营：三列（数值 + 环比/同比）
        self._title_bar(96, "橘喵·今日实时", mao["upd"])
        cols = mao["cols"]; n = len(cols)
        ax0, ax1 = 8, 392
        cw = (ax1 - ax0) / n
        for i in range(1, n):
            self.vdash(ax0 + cw * i, 128, 192)
        for i, c in enumerate(cols):
            cx = ax0 + cw * (i + 0.5)
            self.ctext(cx, 114, c["k"], self.F12)
            self.ctext(cx, 130, c["v"], self.F24, bold=1)
            self._cmp_line(cx, 160, "环比", c["hb"])
            self._cmp_line(cx, 178, "同比", c["tb"])

        self.hdash(8, 392, 200)

        # D. 近30天三指标趋势折线图
        self._trend_chart(204, mao["trend"])

        return self.img

    def render_to_file(self, data, path):
        img = self.render(data)
        import os
        os.makedirs(os.path.dirname(path), exist_ok=True)
        img.save(path)
        return path
