WatchThemeModel.swift 7.8 KB
//
//  WatchThemeModel.swift
//  hippo
//
//  Created by 权海 on 2026/6/26.
//

import Foundation

enum WatchThemeLocalBuilder {
    static func makeLocalTheme(from model: WatchThemeModel) async -> WatchLocalTheme {
        async let energeticImage = WatchThemeImageCache.data(for: model.energeticImageURL)
        async let normalImage = WatchThemeImageCache.data(for: model.normalImageURL)
        async let slightStressfulImage = WatchThemeImageCache.data(for: model.slightStressImageURL)
        async let stressfulImage = WatchThemeImageCache.data(for: model.stressfulImageURL)

        return await WatchLocalTheme(
            themeId: model.id ?? 0,
            themeName: model.themeName ?? "",
            energeticDescription: model.energeticTitle ?? "",
            energeticImage: energeticImage,
            normalDescription: model.normalTitle ?? "",
            normalImage: normalImage,
            slightStressfulDescription: model.slightStressTitle ?? "",
            slightStressfulImage: slightStressfulImage,
            stressfulDescription: model.stressfulTitle ?? "",
            stressfulImage: stressfulImage
        )
    }
}

private enum WatchThemeImageCache {
    static func data(for urlString: String?) async -> Data? {
        guard let urlString, !urlString.isEmpty else { return nil }
        if let cachedData = cachedData(for: urlString) {
            return cachedData
        }
        if let defaultsData = AppGroupConstants.defaults?.data(forKey: urlString) {
            save(defaultsData, for: urlString)
            return defaultsData
        }
        guard let url = URL(string: urlString) else { return nil }

        do {
            let (data, response) = try await URLSession.shared.data(from: url)
            guard let httpResponse = response as? HTTPURLResponse,
                  (200..<300).contains(httpResponse.statusCode) else {
                return nil
            }
            save(data, for: urlString)
            return data
        } catch {
            return nil
        }
    }

    private static func cachedData(for urlString: String) -> Data? {
        let url = cacheURL(for: urlString)
        return try? Data(contentsOf: url)
    }

    private static func save(_ data: Data, for urlString: String) {
        let url = cacheURL(for: urlString)
        do {
            try FileManager.default.createDirectory(
                at: url.deletingLastPathComponent(),
                withIntermediateDirectories: true
            )
            try data.write(to: url, options: [.atomic])
        } catch {
        }
    }

    private static func cacheURL(for urlString: String) -> URL {
        let baseURL = FileManager.default.containerURL(
            forSecurityApplicationGroupIdentifier: AppGroupConstants.identifier
        ) ?? FileManager.default.temporaryDirectory
        return baseURL
            .appendingPathComponent("watch_theme_image_cache", isDirectory: true)
            .appendingPathComponent(stableHash(urlString))
    }

    private static func stableHash(_ value: String) -> String {
        var hash: UInt64 = 14_695_981_039_346_656_037
        for byte in value.utf8 {
            hash ^= UInt64(byte)
            hash &*= 1_099_511_628_211
        }
        return String(format: "%016llx", hash)
    }
}

struct WatchLocalTheme: Codable, Equatable {
    var themeId: Int
    var themeName: String
    var energeticDescription: String
    var energeticImage: Data?
    var normalDescription: String
    var normalImage: Data?
    var slightStressfulDescription: String
    var slightStressfulImage: Data?
    var stressfulDescription: String
    var stressfulImage: Data?

    init(
        themeId: Int,
        themeName: String,
        energeticDescription: String,
        energeticImage: Data?,
        normalDescription: String,
        normalImage: Data?,
        slightStressfulDescription: String,
        slightStressfulImage: Data?,
        stressfulDescription: String,
        stressfulImage: Data?
    ) {
        self.themeId = themeId
        self.themeName = themeName
        self.energeticDescription = energeticDescription
        self.energeticImage = energeticImage
        self.normalDescription = normalDescription
        self.normalImage = normalImage
        self.slightStressfulDescription = slightStressfulDescription
        self.slightStressfulImage = slightStressfulImage
        self.stressfulDescription = stressfulDescription
        self.stressfulImage = stressfulImage
    }

    init(model: WatchThemeModel) {
        self.init(
            themeId: model.id ?? 0,
            themeName: model.themeName ?? "",
            energeticDescription: model.energeticTitle ?? "",
            energeticImage: nil,
            normalDescription: model.normalTitle ?? "",
            normalImage: nil,
            slightStressfulDescription: model.slightStressTitle ?? "",
            slightStressfulImage: nil,
            stressfulDescription: model.stressfulTitle ?? "",
            stressfulImage: nil
        )
    }

    init?(payload: [String: Any]) {
        let rawThemeId = payload["theme_id"]
        let themeId: Int
        if let value = rawThemeId as? Int {
            themeId = value
        } else if let value = rawThemeId as? Int64 {
            themeId = Int(value)
        } else if let value = rawThemeId as? NSNumber {
            themeId = value.intValue
        } else {
            return nil
        }

        self.init(
            themeId: themeId,
            themeName: payload["theme_name"] as? String ?? "",
            energeticDescription: payload["energetic_description"] as? String ?? "",
            energeticImage: Self.imageData(from: payload["energetic_image"]),
            normalDescription: payload["normal_description"] as? String ?? "",
            normalImage: Self.imageData(from: payload["normal_image"]),
            slightStressfulDescription: payload["slight_stressful_description"] as? String ?? "",
            slightStressfulImage: Self.imageData(from: payload["slight_stressful_image"]),
            stressfulDescription: payload["stressful_description"] as? String ?? "",
            stressfulImage: Self.imageData(from: payload["stressful_image"])
        )
    }

    private static func imageData(from value: Any?) -> Data? {
        if let data = value as? Data { return data }
        if let data = value as? NSData { return data as Data }
        return nil
    }

    var hasAllImages: Bool {
        energeticImage != nil &&
        normalImage != nil &&
        slightStressfulImage != nil &&
        stressfulImage != nil
    }
}

struct WatchThemeModel: Codable, Equatable {
    var id: Int?
    var userId: Int?
    var themeName: String?
    var isOfficial: Int?
    
    var energeticTitle: String?
    var energeticImageURL: String?
    
    var normalTitle: String?
    var normalImageURL: String?
    
    var slightStressTitle: String?
    var slightStressImageURL: String?
    
    var stressfulTitle: String?
    var stressfulImageURL: String?
    
    static func == (lhs: Self, rhs: Self) -> Bool{
        lhs.energeticImageURL == rhs.energeticImageURL &&
        lhs.normalImageURL == rhs.normalImageURL &&
        lhs.slightStressImageURL == rhs.slightStressImageURL &&
        lhs.stressfulImageURL == rhs.stressfulImageURL
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case userId =   "user_id"
        case themeName  =   "theme_name"
        case isOfficial =   "is_official"
        case energeticTitle =   "energetic_description"
        case energeticImageURL  =   "energetic_image"
        case normalTitle    =   "normal_description"
        case normalImageURL =   "normal_image"
        case slightStressTitle  =   "slight_stressful_description"
        case slightStressImageURL   =   "slight_stressful_image"
        case stressfulTitle =   "stressful_description"
        case stressfulImageURL  =   "stressful_image"
    }
}