//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit

public extension Notification.Name {
    static let themeDidChange = Notification.Name("ThemeDidChangeNotification")
}

@objc
public extension NSNotification {
    static var ThemeDidChange: NSString { Notification.Name.themeDidChange.rawValue as NSString }
}

final public class Theme: NSObject {

    public enum Mode: UInt {
        case system, light, dark
    }

    private static var shared = Theme()

    private override init() {
        super.init()

        AppReadiness.runNowOrWhenAppDidBecomeReadySync {
            // IOS-782: +[Theme shared] re-enterant initialization
            // AppReadiness will invoke the block synchronously if the app is already ready.
            // This doesn't work here, because we'll end up reenterantly calling +shared
            // if the app is in dark mode and the first call to +[Theme shared] happens
            // after the app is ready.
            //
            // It looks like that pattern is only hit in the share extension, but we're better off
            // asyncing always to ensure the dependency chain is broken. We're okay waiting, since
            // there's no guarantee that this block in synchronously executed anyway.
            DispatchQueue.main.async {
                self.notifyIfThemeModeIsNotDefault()
            }
        }
    }

    private static var keyValueStore: SDSKeyValueStore {
        return SDSKeyValueStore(collection: "ThemeCollection")
    }

    private struct KVSKeys {
        static var currentMode = "ThemeKeyCurrentMode"
        static var legacyThemeEnabled = "ThemeKeyThemeEnabled"
    }

    public class func setupSignalAppearance() {
        UINavigationBar.appearance().barTintColor = Theme.navbarBackgroundColor
        UINavigationBar.appearance().tintColor = Theme.primaryIconColor
        UIToolbar.appearance().barTintColor = Theme.navbarBackgroundColor
        UIToolbar.appearance().tintColor = Theme.primaryIconColor

        // We do _not_ specify BarButton.appearance().tintColor because it is sufficient to specify
        // UINavigationBar.appearance().tintColor. Furthermore, specifying the BarButtonItem's
        // appearance makes it more difficult to override the navbar theme, e.g. how we _always_
        // use dark theme in the media send flow and gallery views. If we were specifying
        // barButton.appearance().tintColor we would then have to manually override each
        // BarButtonItem's tint, rather than just the navbars.
        //
        // UIBarButtonItem.appearance.tintColor = Theme.primaryIconColor;

        // Using UIText{View,Field}.appearance().keyboardAppearance crashes due to a bug in UIKit,
        // so we don't do it.

        UITableViewCell.appearance().tintColor = Theme.primaryIconColor
        UIToolbar.appearance().tintColor = .ows_accentBlue

        // If we set NSShadowAttributeName, the NSForegroundColorAttributeName value is ignored.
        UINavigationBar.appearance().titleTextAttributes = [
            NSAttributedString.Key.foregroundColor: Theme.navbarTitleColor
        ]

        let cursorColor = isDarkThemeEnabled ? UIColor.white : UIColor.ows_accentBlue
        UITextView.appearance(whenContainedInInstancesOf: [OWSNavigationController.self]).tintColor = cursorColor
        UITextField.appearance(whenContainedInInstancesOf: [OWSNavigationController.self]).tintColor = cursorColor
    }

    // MARK: - Theme Mode

    public static var isDarkThemeEnabled: Bool { shared.isDarkThemeEnabled }

    public class func getOrFetchCurrentMode() -> Mode {
        return shared.getOrFetchCurrentMode()
    }

    public class func setCurrentMode(_ mode: Mode) {
        shared.setCurrentMode(mode)
    }

    public class func performWithModeAsCurrent(_ mode: Mode, _ operation: () -> Void) {
        shared.performWithModeAsCurrent(mode, operation)
    }

    private func performWithModeAsCurrent(_ mode: Mode, _ operation: () -> Void) {
        let previousMode = cachedCurrentMode
        defer { cachedCurrentMode = previousMode }
        cachedCurrentMode = mode
        operation()
    }

    private var cachedIsDarkThemeEnabled: Bool?
    private var cachedCurrentMode: Mode?

    private var isDarkThemeEnabled: Bool {
#if TESTABLE_BUILD
        if let isDarkThemeEnabledForTests {
            return isDarkThemeEnabledForTests
        }
#endif

        // Don't cache this value until it reflects the data store.
        guard AppReadiness.isAppReady else {
            return isSystemDarkThemeEnabled()
        }

        // Always respect the system theme in extensions.
        guard CurrentAppContext().isMainApp else {
            return isSystemDarkThemeEnabled()
        }

        if let cachedIsDarkThemeEnabled {
            return cachedIsDarkThemeEnabled
        }

        let isDarkThemeEnabled: Bool = {
            switch getOrFetchCurrentMode() {
            case .system: return isSystemDarkThemeEnabled()
            case .dark: return true
            case .light: return false
            }
        }()
        cachedIsDarkThemeEnabled = isDarkThemeEnabled

        return isDarkThemeEnabled
    }

    private func isSystemDarkThemeEnabled() -> Bool {
        return UITraitCollection.current.userInterfaceStyle == .dark
    }

    private func getOrFetchCurrentMode() -> Mode {
        if let cachedCurrentMode {
            return cachedCurrentMode
        }

        guard AppReadiness.isAppReady else {
            return defaultMode
        }

        var currentMode: Mode = .system
        databaseStorage.read { transaction in
            let hasDefinedMode = Theme.keyValueStore.hasValue(forKey: KVSKeys.currentMode, transaction: transaction)
            if hasDefinedMode {
                let rawMode = Theme.keyValueStore.getUInt(
                    KVSKeys.currentMode,
                    defaultValue: Theme.Mode.system.rawValue,
                    transaction: transaction
                )
                if let definedMode = Mode(rawValue: rawMode) {
                    currentMode = definedMode
                }
            } else {
                // If the theme has not yet been defined, check if the user ever manually changed
                // themes in a legacy app version. If so, preserve their selection. Otherwise,
                // default to matching the system theme.
                if Theme.keyValueStore.hasValue(forKey: KVSKeys.legacyThemeEnabled, transaction: transaction) {
                    let isLegacyModeDark = Theme.keyValueStore.getBool(
                        KVSKeys.legacyThemeEnabled,
                        defaultValue: false,
                        transaction: transaction
                    )
                    currentMode = isLegacyModeDark ? .dark : .light
                }
            }
        }

        cachedCurrentMode = currentMode

        return currentMode
    }

    private func setCurrentMode(_ mode: Mode) {
        AssertIsOnMainThread()

        let previousMode = cachedCurrentMode

        switch mode {
        case .light:
            cachedIsDarkThemeEnabled = false
        case .dark:
            cachedIsDarkThemeEnabled = true
        case .system:
            cachedIsDarkThemeEnabled = isSystemDarkThemeEnabled()
        }

        cachedCurrentMode = mode

        // It's safe to do an async write because all accesses check self.cachedCurrentThemeNumber first.
        databaseStorage.asyncWrite { transaction in
            Theme.keyValueStore.setUInt(mode.rawValue, key: KVSKeys.currentMode, transaction: transaction)
        }

        if previousMode != mode {
            themeDidChange()
        }
    }

    private var defaultMode: Mode {
        return .system
    }

    private func notifyIfThemeModeIsNotDefault() {
        if isDarkThemeEnabled || defaultMode != getOrFetchCurrentMode() {
            themeDidChange()
        }
    }

    // MARK: -

    class func systemThemeChanged() {
        shared.systemThemeChanged()
    }

    private func systemThemeChanged() {
        // Do nothing, since we haven't setup the theme yet.
        guard cachedIsDarkThemeEnabled != nil else { return }

        // Theme can only be changed externally when in system mode.
        guard getOrFetchCurrentMode() == .system else { return }

        // We may get multiple updates for the same change.
        let isSystemDarkThemeEnabled = isSystemDarkThemeEnabled()
        guard cachedIsDarkThemeEnabled != isSystemDarkThemeEnabled else { return }

        cachedIsDarkThemeEnabled = isSystemDarkThemeEnabled
        themeDidChange()
    }

    private func themeDidChange() {
        Theme.setupSignalAppearance()
        NotificationCenter.default.post(name: .themeDidChange, object: self)
    }

    // MARK: - UI Colors

    @objc
    public class var backgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeBackgroundColor : .white
    }

    public class var secondaryBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeSecondaryBackgroundColor : lightThemeSecondaryBackgroundColor
    }

    public class var lightThemeSecondaryBackgroundColor: UIColor {
        .ows_gray02
    }

    public class var darkThemeSecondaryBackgroundColor: UIColor {
        .ows_gray80
    }

    public class var washColor: UIColor {
        isDarkThemeEnabled ? darkThemeWashColor : .ows_gray05
    }

    public class var primaryTextColor: UIColor {
        isDarkThemeEnabled ? darkThemePrimaryColor : lightThemePrimaryColor
    }

    public class var primaryIconColor: UIColor {
        isDarkThemeEnabled ? darkThemeNavbarIconColor : .ows_gray75
    }

    public class var secondaryTextAndIconColor: UIColor {
        isDarkThemeEnabled ? darkThemeSecondaryTextAndIconColor : lightThemeSecondaryTextAndIconColor
    }

    public class var ternaryTextColor: UIColor { .ows_gray45 }

    public class var placeholderColor: UIColor { .ows_gray45 }

    public class var snippetColor: UIColor {
        isDarkThemeEnabled ? darkThemeSnippetColor : lightThemeSnippetColor
    }

    public class var hairlineColor: UIColor {
        isDarkThemeEnabled ? .ows_gray75 : .ows_gray15
    }

    public class var outlineColor: UIColor {
        isDarkThemeEnabled ? .ows_gray75 : .ows_gray15
    }

    public class var backdropColor: UIColor { .ows_blackAlpha40 }

    public class var navbarBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeNavbarBackgroundColor : .white
    }

    public class var navbarTitleColor: UIColor { primaryTextColor }

    public class var toolbarBackgroundColor: UIColor { navbarBackgroundColor }

    // For accessibility:
    //
    // * Flat areas (e.g. button backgrounds) should use UIColor.ows_accentBlue.
    // * Fine detail (e.g., text, non-filled icons) should use Theme.accentBlueColor.
    //   It is brighter in dark mode, improving legibility.
    public class var accentBlueColor: UIColor {
        isDarkThemeEnabled ? .ows_accentBlueDark : .ows_accentBlue
    }

    public class var conversationButtonBackgroundColor: UIColor {
        isDarkThemeEnabled ? .ows_gray80 : .ows_gray02
    }

    public class var conversationButtonTextColor: UIColor {
        isDarkThemeEnabled ? .ows_gray05 : .ows_accentBlue
    }

    @objc
    public class var launchScreenBackgroundColor: UIColor {
        return isDarkThemeEnabled ? .ows_signalBlueDark : .ows_signalBlue
    }

    // MARK: - Table View

    public class var cellSelectedColor: UIColor {
        isDarkThemeEnabled ? UIColor(white: 0.2, alpha: 1) : UIColor(white: 0.92, alpha: 1)
    }

    public class var cellSeparatorColor: UIColor { hairlineColor }

    public class var tableCell2BackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2BackgroundColor : .white
    }

    public class var tableCell2PresentedBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2PresentedBackgroundColor : .white
    }

    public class var tableCell2SelectedBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2SelectedBackgroundColor : .ows_gray15
    }

    public class var tableCell2SelectedBackgroundColor2: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2SelectedBackgroundColor2 : .ows_gray15
    }

    public class var tableCell2MultiSelectedBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2MultiSelectedBackgroundColor : .ows_gray05
    }

    public class var tableCell2PresentedSelectedBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableCell2PresentedSelectedBackgroundColor : .ows_gray15
    }

    public class var tableView2BackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableView2BackgroundColor : .ows_gray10
    }

    public class var tableView2PresentedBackgroundColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableView2PresentedBackgroundColor : .ows_gray10
    }

    public class var tableView2SeparatorColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableView2SeparatorColor : .ows_gray20
    }

    public class var tableView2PresentedSeparatorColor: UIColor {
        isDarkThemeEnabled ? darkThemeTableView2PresentedSeparatorColor : .ows_gray20
    }

    // MARK: - Light Theme Colors

    public class var lightThemePrimaryColor: UIColor { .ows_gray90 }

    public class var lightThemeSecondaryTextAndIconColor: UIColor { .ows_gray60 }

    public class var lightThemeSnippetColor: UIColor { .ows_gray45 }

    // MARK: - Dark Theme Colors

    public class var darkThemeBackgroundColor: UIColor { .black }

    public class var darkThemePrimaryColor: UIColor { .ows_gray02 }

    public class var darkThemeSecondaryTextAndIconColor: UIColor { .ows_gray25 }

    public class var darkThemeSnippetColor: UIColor { .ows_gray25 }

    public class var darkThemeWashColor: UIColor { .ows_gray75 }

    public class var darkThemeNavbarBackgroundColor: UIColor { .black }

    public class var darkThemeNavbarIconColor: UIColor { .ows_gray15 }

    public class var darkThemeTableCell2BackgroundColor: UIColor { .ows_gray90 }

    public class var darkThemeTableCell2PresentedBackgroundColor: UIColor { .ows_gray80 }

    public class var darkThemeTableCell2SelectedBackgroundColor: UIColor { .ows_gray80 }

    public class var darkThemeTableCell2SelectedBackgroundColor2: UIColor { .ows_gray65 }

    public class var darkThemeTableCell2MultiSelectedBackgroundColor: UIColor { .ows_gray75 }

    public class var darkThemeTableCell2PresentedSelectedBackgroundColor: UIColor { .ows_gray75 }

    public class var darkThemeTableView2BackgroundColor: UIColor { .black }

    public class var darkThemeTableView2PresentedBackgroundColor: UIColor { .ows_gray90 }

    public class var darkThemeTableView2SeparatorColor: UIColor { .ows_gray75 }

    public class var darkThemeTableView2PresentedSeparatorColor: UIColor { .ows_gray65 }

    // MARK: - Blur Effect

    public class var barBlurEffect: UIBlurEffect {
        isDarkThemeEnabled ? darkThemeBarBlurEffect : UIBlurEffect(style: .light)
    }

    public class var darkThemeBarBlurEffect: UIBlurEffect { UIBlurEffect(style: .dark) }

    // MARK: - Keyboard

    public class var keyboardAppearance: UIKeyboardAppearance {
        isDarkThemeEnabled ? darkThemeKeyboardAppearance : .default
    }

    public class var darkThemeKeyboardAppearance: UIKeyboardAppearance { .dark }

    public class var keyboardBackgroundColor: UIColor {
        isDarkThemeEnabled ? .ows_gray90 : .ows_gray02
    }

    // MARK: - Search Bar

    public class var barStyle: UIBarStyle {
        isDarkThemeEnabled ? .black : .default
    }

    public class var searchFieldBackgroundColor: UIColor { washColor }

    public class var searchFieldElevatedBackgroundColor: UIColor {
        isDarkThemeEnabled ? .ows_gray75 : .ows_gray12
    }

#if TESTABLE_BUILD
    private var isDarkThemeEnabledForTests: Bool?

    public class func setIsDarkThemeEnabledForTests(_ enabled: Bool) {
        shared.isDarkThemeEnabledForTests = enabled
    }
#endif
}
