using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using TeamAAS;
namespace TeamAAS.Theme
{
///
/// 主题画刷覆盖层:主色驱动的专业配色系统核心。
///
/// 原理(对齐 HandyControls fork 的 ThemeResources 机制):
/// 把一张【纯值覆盖字典】Add 到应用合并字典【末尾】——WPF 合并字典查找为
/// "后添加的先查"(逆序),覆盖层获得最高查找优先级,元素/样式重查任何
/// 画刷/调色板键都命中这里的纯值,官方大字典被遮蔽。
///
/// 构成:
/// 1. 官方基准(全量):启动/整替官方字典后,把调色板 Color 键与画刷以
/// "求值冻结"形式捕获为纯值基准(此时求值链干净,值得官方色彩真传);
/// 中性色/语义色沿用官方值,保证与 HC 控件生态协调。
/// 2. 主色派生(专业配色):PrimaryBrush/DarkPrimaryBrush/TitleBrush 及对应 Color 键
/// 由用户主色按 HSL 派生(暗色下主色提亮保对比、hover 变体 darken 12%),
/// ReverseTextColor 按主色亮度自动选黑/白(WCAG AA 可读性)。
///
/// 收益:
/// - 覆盖画刷是纯值(无 DynamicResource 表达式)→ 天然免疫第三方资源污染;
/// - 明暗/主色切换 = 重建轻量覆盖字典(毫秒级)→ 不触碰官方大字典 → 无全窗口样式重算(丝滑);
/// - 明暗基准按 dark/light 各缓存一份,首次遇到某明暗需整替官方字典捕获,之后纯覆盖切换。
///
internal static class ThemeOverlay
{
/// HC 主色系画刷键:值由用户主色派生(而非官方基准)。
private static readonly string[] AccentDrivenBrushes =
{ "PrimaryBrush", "DarkPrimaryBrush", "TitleBrush" };
/// HC 主色系调色板键:与画刷同步派生。
private static readonly string[] AccentDrivenColors =
{ "PrimaryColor", "DarkPrimaryColor" };
/// 当前覆盖字典(Purge 放行判据;Add 于应用合并字典末尾)。
private static ResourceDictionary _overlay;
/// 官方基准缓存:明暗 → (键 → 官方纯值)。官方值来自整替后的干净字典。
private static readonly Dictionary> _baseCache =
new Dictionary>();
/// 当前明暗(整替兜底路径的默认值来源)。
public static bool CurrentDark { get; private set; }
/// 当前主色(整替兜底路径重建覆盖时的来源)。
public static Color CurrentAccent { get; private set; } = Color.FromRgb(0x2D, 0x6C, 0xDF);
/// 官方基准是否已捕获(某明暗)。
public static bool HasBase(bool dark)
{
return _baseCache.ContainsKey(dark);
}
///
/// 捕获官方基准:在官方字典刚被整替(干净、未被污染)时调用。
/// 递归收集白名单官方字典中的 Brush(求值冻结)与 Color 键。
///
public static void CaptureBase(bool dark)
{
var app = Application.Current;
if (app == null) return;
try
{
var baseDict = new Dictionary(StringComparer.Ordinal);
CollectOfficial(app.Resources, baseDict);
_baseCache[dark] = baseDict;
AppLogger.Info(
$"[主题覆盖] 已捕获 {(dark ? "暗色" : "浅色")}官方基准({baseDict.Count} 项)",
nameof(ThemeOverlay));
}
catch (Exception ex)
{
AppLogger.Error("[主题覆盖] 官方基准捕获失败", ex, nameof(ThemeOverlay));
}
}
/// 递归收集官方字典中的 Brush(冻结纯值)与 Color 键。
private static void CollectOfficial(ResourceDictionary dict, Dictionary into)
{
// 只认官方字典(第三方注入字典由 Purge 负责移除,不进基准)
var merged = dict.MergedDictionaries;
for (int i = 0; i < merged.Count; i++)
{
var child = merged[i];
if (IsOfficialDict(child)) CollectOfficial(child, into);
}
foreach (var key in dict.Keys)
{
var keyName = key as string;
if (keyName == null) continue;
object value;
try { value = dict[key]; }
catch { continue; }
var brush = value as Brush;
if (brush != null)
{
try { into[keyName] = brush.GetCurrentValueAsFrozen(); } // 求值冻结:官方当前色纯值
catch { /* 求值失败(如被污染)跳过,该键保持未覆盖 */ }
continue;
}
if (value is Color) into[keyName] = value; // 调色板 Color 键
}
}
///
/// 构建并应用覆盖字典(明暗/主色变化的统一入口,毫秒级,丝滑)。
/// 若该明暗的官方基准尚未捕获(首次遇到),返回 false 由调用方先整替官方字典。
///
public static bool Apply(bool dark, Color accent)
{
Dictionary baseDict;
if (!_baseCache.TryGetValue(dark, out baseDict)) return false;
var app = Application.Current;
if (app == null) return true;
try
{
// 记录当前主题状态(整替兜底路径的恢复来源)
CurrentDark = dark;
CurrentAccent = accent;
// 主色在暗色下轻微提亮,保证在深背景上的对比度(专业配色的明暗适配)
var effectiveAccent = dark ? ColorUtility.Lighten(accent, 0.08) : accent;
var hoverAccent = ColorUtility.Darken(effectiveAccent, 0.12);
var textOnAccent = ColorUtility.ContrastText(effectiveAccent);
var overlay = new ResourceDictionary();
// 1) 官方基准全量(纯值:中性色/语义色沿用官方,天然协调)
foreach (var kv in baseDict)
{
overlay[kv.Key] = kv.Value;
}
// 2) 主色系派生(覆盖基准中的官方主色)
foreach (var key in AccentDrivenBrushes)
{
if (!baseDict.ContainsKey(key)) continue;
Color c;
if (key == "DarkPrimaryBrush") c = hoverAccent;
else c = effectiveAccent;
var brush = new SolidColorBrush(c);
brush.Freeze();
overlay[key] = brush;
}
foreach (var key in AccentDrivenColors)
{
if (!baseDict.ContainsKey(key)) continue;
overlay[key] = key == "DarkPrimaryColor" ? hoverAccent : effectiveAccent;
}
// 3) 主色上的文字对比色:写入 ReverseTextColor(HC 官方语义即"反色文字",用于
// 主色背景上的按钮文字等),按主色亮度自动选黑/白(避免浅色主色+白字看不清)。
// 【注意】绝不能动 PrimaryTextBrush——那是全局正文主文本色,明暗适配由官方
// 调色板负责;若按主色派生会让浅色主题正文全变白字(白底白字,实测 2026-09-04)
if (baseDict.ContainsKey("ReverseTextColor"))
{
var brush = new SolidColorBrush(textOnAccent);
brush.Freeze();
overlay["ReverseTextColor"] = brush;
}
// 4) 替换/插入覆盖层。
// 【关键】WPF 合并字典的查找顺序是"后添加的先查"(逆序):
// 覆盖层必须 Add 到合并字典【末尾】才能获得最高查找优先级;
// 若 Insert(0) 放最前面,反而会被 Theme.xaml 的旧画刷遮蔽(实测:切换后颜色不动)
var merged = app.Resources.MergedDictionaries;
if (_overlay != null) merged.Remove(_overlay);
_overlay = overlay;
merged.Add(_overlay);
}
catch (Exception ex)
{
AppLogger.Error("[主题覆盖] 覆盖字典构建失败", ex, nameof(ThemeOverlay));
}
return true;
}
/// Purge 放行判据:是否我们的覆盖字典。
public static bool IsOverlay(ResourceDictionary dict)
{
return dict != null && ReferenceEquals(dict, _overlay);
}
///
/// 摘除覆盖层(不重建)。整替官方字典前必须调用——
/// 否则基准捕获会被上一轮覆盖值污染(覆盖层查找优先级最高)。
///
public static void RemoveOverlay()
{
if (_overlay == null) return;
try { Application.Current?.Resources.MergedDictionaries.Remove(_overlay); }
catch { /* 移除失败忽略 */ }
_overlay = null;
}
/// 官方字典判定(与 VmResourceGuard 同规则:Source 含 HandyControl)。
private static bool IsOfficialDict(ResourceDictionary dict)
{
try
{
var src = dict.Source;
return src != null && src.OriginalString.Contains("HandyControl");
}
catch
{
return false;
}
}
}
}