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

import SignalServiceKit

// An immutable snapshot of the core styling
// state used by CVC for a given load/render cycle.
public class ConversationStyle: NSObject {

    public enum `Type`: UInt {
        // The style used from initialization until presentation begins.
        // It does not have valid values and should not be rendered.
        case initial
        // The style used until the presentation has configured the view(s).
        // It has values inferred from the navigationController.
        case placeholder
        // The style once presentation has configured the view(s).
        // It has values from the CVC view state.
        case `default`
        // The style used in the message detail view
        case messageDetails

        fileprivate var isValid: Bool { self != .initial }
    }

    public let type: `Type`

    public var isValidStyle: Bool { type.isValid }

    // The width of the collection view.
    public let viewWidth: CGFloat

    public let isDarkThemeEnabled: Bool

    public let hasWallpaper: Bool

    public let isWallpaperPhoto: Bool

    private let dynamicBodyTypePointSize: CGFloat
    private let primaryTextColor: UIColor

    public let contentMarginTop: CGFloat = 24
    public let contentMarginBottom: CGFloat = 24

    public let gutterLeading: CGFloat
    public let gutterTrailing: CGFloat

    public let headerGutterLeading: CGFloat = 28
    public let headerGutterTrailing: CGFloat = 28

    // These are the gutters used by "full width" views
    // like "contact offer" and "info message".
    public let fullWidthGutterLeading: CGFloat
    public let fullWidthGutterTrailing: CGFloat

    static public let groupMessageAvatarSizeClass = ConversationAvatarView.Configuration.SizeClass.twentyEight
    static public let selectionViewWidth: CGFloat = 24
    static public let messageStackSpacing: CGFloat = 8
    static public let defaultMessageSpacing: CGFloat = 12
    static public let compactMessageSpacing: CGFloat = 2
    static public let systemMessageSpacing: CGFloat = 20

    public let contentWidth: CGFloat

    public var headerViewContentWidth: CGFloat {
        viewWidth - (headerGutterLeading + headerGutterTrailing)
    }

    public let maxMessageWidth: CGFloat
    public let maxMediaMessageWidth: CGFloat
    public let maxAudioMessageWidth: CGFloat

    public let textInsetTop: CGFloat
    public let textInsetBottom: CGFloat
    public let textInsetHorizontal: CGFloat
    public var textInsets: UIEdgeInsets {
        UIEdgeInsets(top: textInsetTop,
                     leading: textInsetHorizontal,
                     bottom: textInsetBottom,
                     trailing: textInsetHorizontal)
    }

    // We want to align "group sender" avatars with the v-center of the
    // "last line" of the message body text - or where it would be for
    // non-text content.
    //
    // This is the distance from that v-center to the bottom of the
    // message bubble.
    public let lastTextLineAxis: CGFloat

    // Incoming and outgoing messages are visually distinguished
    // by leading and trailing alignment, respectively.
    //
    // Reserve a space so that in the most compressed layouts
    // (small form factor, group avatar, multi-select, etc.)
    // there is space for them to align in these directions.
    public static let messageDirectionSpacing: CGFloat = 12

    // ColorOrGradientSetting is used for persistence, logging and comparison.
    public let chatColorSetting: ColorOrGradientSetting
    // ColorOrGradientValue is used for rendering.
    public let chatColorValue: ColorOrGradientValue

    public init(
        type: `Type`,
        thread: TSThread,
        viewWidth: CGFloat,
        hasWallpaper: Bool,
        isWallpaperPhoto: Bool,
        chatColor: ColorOrGradientSetting
    ) {
        self.type = type
        self.viewWidth = viewWidth
        self.isDarkThemeEnabled = Theme.isDarkThemeEnabled
        self.primaryTextColor = Theme.primaryTextColor
        self.hasWallpaper = hasWallpaper
        self.isWallpaperPhoto = isWallpaperPhoto
        self.chatColorSetting = chatColor
        self.chatColorValue = chatColor.asValue

        if type == .messageDetails {
            gutterLeading = 0
            gutterTrailing = 0
            fullWidthGutterLeading = 0
            fullWidthGutterTrailing = 0
        } else {
            gutterLeading = thread.isGroupThread ? 12 : 16
            gutterTrailing = 16

            fullWidthGutterLeading = thread.isGroupThread ? 12 : 16
            fullWidthGutterTrailing = thread.isGroupThread ? 12 : 16
        }

        let messageTextFont = UIFont.dynamicTypeBody

        dynamicBodyTypePointSize = messageTextFont.pointSize

        let baseFontOffset: CGFloat = 11

        // Don't include the distance from the "cap height" to the top of the UILabel
        // in the top margin.
        textInsetTop = max(0, round(baseFontOffset - (messageTextFont.ascender - messageTextFont.capHeight)))
        // Don't include the distance from the "baseline" to the bottom of the UILabel
        // (e.g. the descender) in the top margin. Note that UIFont.descender is a
        // negative value.
        textInsetBottom = max(0, round(baseFontOffset - abs(messageTextFont.descender)))

        textInsetHorizontal = 12

        lastTextLineAxis = CGFloat(round(baseFontOffset + messageTextFont.capHeight * 0.5))

        contentWidth = viewWidth - (gutterLeading + gutterTrailing)

        var maxMessageWidth = contentWidth - (Self.selectionViewWidth + Self.messageStackSpacing)

        maxMessageWidth -= Self.messageDirectionSpacing

        if thread.isGroupThread {
            maxMessageWidth -= (CGFloat(ConversationStyle.groupMessageAvatarSizeClass.size.width) + Self.messageStackSpacing)
        }
        self.maxMessageWidth = maxMessageWidth

        // This upper bound should have no effect in portrait orientation.
        // It limits body media size in landscape.
        let kMaxBodyMediaSize: CGFloat = 350
        maxMediaMessageWidth = floor(min(maxMessageWidth, kMaxBodyMediaSize))

        let kMaxAudioMessageWidth: CGFloat = 244
        maxAudioMessageWidth = floor(min(maxMessageWidth, kMaxAudioMessageWidth))

        super.init()
    }

    // MARK: Colors

    public static func bubbleColorIncoming(hasWallpaper: Bool,
                                           isDarkThemeEnabled: Bool) -> UIColor {
        if hasWallpaper {
            return isDarkThemeEnabled ? .ows_gray95 : .white
        } else {
            return isDarkThemeEnabled ? UIColor.ows_gray80 : UIColor.ows_gray05
        }
    }
    public var bubbleColorIncoming: UIColor {
        Self.bubbleColorIncoming(hasWallpaper: hasWallpaper,
                                 isDarkThemeEnabled: isDarkThemeEnabled)
    }

    public let dateBreakTextColor = UIColor.ows_gray60

    public func bubbleChatColor(isIncoming: Bool) -> ColorOrGradientValue {
        if isIncoming {
            return .solidColor(color: bubbleColorIncoming)
        } else {
            return bubbleChatColorOutgoing
        }
    }

    public var bubbleChatColorOutgoing: ColorOrGradientValue {
        chatColorValue
    }

    public static var bubbleTextColorIncoming: UIColor {
        return bubbleTextColorIncomingThemed.forCurrentTheme
    }

    public static var bubbleTextColorIncomingThemed: ThemedColor {
        ThemedColor(light: UIColor.ows_gray90, dark: UIColor.ows_gray05)
    }

    public static var bubbleTextColorOutgoing: UIColor {
        return bubbleTextColorOutgoingThemed.forCurrentTheme
    }

    public static var bubbleTextColorOutgoingThemed: ThemedColor {
        ThemedColor(light: UIColor.ows_white, dark: UIColor.ows_gray05)
    }

    public var bubbleTextColorIncoming: UIColor {
        Self.bubbleTextColorIncomingThemed.color(isDarkThemeEnabled: isDarkThemeEnabled)
    }

    public var bubbleSecondaryTextColorIncoming: UIColor {
        isDarkThemeEnabled ? .ows_gray25 : .ows_gray60
    }

    public var bubbleTextColorOutgoing: UIColor {
        Self.bubbleTextColorOutgoingThemed.color(isDarkThemeEnabled: isDarkThemeEnabled)
    }

    public var bubbleSecondaryTextColorOutgoing: UIColor {
        isDarkThemeEnabled ? .ows_whiteAlpha60 : .ows_whiteAlpha80
    }

    public func bubbleTextColor(message: TSMessage) -> UIColor {
        if message.wasRemotelyDeleted && !hasWallpaper {
            return primaryTextColor
        } else if message is TSIncomingMessage {
            return bubbleTextColorIncoming
        } else if message is TSOutgoingMessage {
            return bubbleTextColorOutgoing
        } else {
            owsFailDebug("Unexpected message type: \(message)")
            return bubbleTextColorOutgoing
        }
    }

    public func bubbleTextColor(isIncoming: Bool) -> UIColor {
        if isIncoming {
            return bubbleTextColorIncoming
        } else {
            return bubbleTextColorOutgoing
        }
    }

    public func bubbleReadMoreTextColor(message: TSMessage) -> UIColor {
        if message is TSIncomingMessage {
            return isDarkThemeEnabled ? .ows_whiteAlpha90 : .ows_accentBlue
        } else if message is TSOutgoingMessage {
            return isDarkThemeEnabled ? .ows_whiteAlpha90 : .white
        } else {
            owsFailDebug("Unexpected message type: \(message)")
            return bubbleTextColorOutgoing
        }
    }

    public func bubbleSecondaryTextColor(isIncoming: Bool) -> UIColor {
        if isIncoming {
            return bubbleSecondaryTextColorIncoming
        } else {
            return bubbleSecondaryTextColorOutgoing
        }
    }

    // Same across all themes
    public static var searchMatchHighlightColor: UIColor {
        return UIColor.yellow
    }

    public func isEqualForCellRendering(_ other: ConversationStyle) -> Bool {
        // We need to compare any state that could affect
        // how we render view appearance.
        (type.isValid == other.type.isValid &&
            viewWidth == other.viewWidth &&
            dynamicBodyTypePointSize == other.dynamicBodyTypePointSize &&
            isDarkThemeEnabled == other.isDarkThemeEnabled &&
            hasWallpaper == other.hasWallpaper &&
            isWallpaperPhoto == other.isWallpaperPhoto &&
            maxMessageWidth == other.maxMessageWidth &&
            maxMediaMessageWidth == other.maxMediaMessageWidth &&
            textInsets == other.textInsets &&
            gutterLeading == other.gutterLeading &&
            gutterTrailing == other.gutterTrailing &&
            fullWidthGutterLeading == other.fullWidthGutterLeading &&
            fullWidthGutterTrailing == other.fullWidthGutterTrailing &&
            textInsets == other.textInsets &&
            lastTextLineAxis == other.lastTextLineAxis &&
            chatColorSetting == other.chatColorSetting)
    }

    public override var debugDescription: String {
        "[" +
            "type.isValid: \(type.isValid), " +
            "viewWidth: \(viewWidth), " +
            "dynamicBodyTypePointSize: \(dynamicBodyTypePointSize), " +
            "isDarkThemeEnabled: \(isDarkThemeEnabled), " +
            "hasWallpaper: \(hasWallpaper), " +
            "isWallpaperPhoto: \(isWallpaperPhoto), " +
            "maxMessageWidth: \(maxMessageWidth), " +
            "maxMediaMessageWidth: \(maxMediaMessageWidth), " +
            "textInsets: \(textInsets), " +
            "gutterLeading: \(gutterLeading), " +
            "gutterTrailing: \(gutterTrailing), " +
            "fullWidthGutterLeading: \(fullWidthGutterLeading), " +
            "fullWidthGutterTrailing: \(fullWidthGutterTrailing), " +
            "textInsets: \(textInsets), " +
            "lastTextLineAxis: \(lastTextLineAxis), " +
            "chatColor: \(chatColorSetting), " +
            "]"
    }
}

extension ConversationStyle {

    public func quotedReplyHighlightColor() -> UIColor {
        UIColor.init(rgbHex: 0xB5B5B5)
    }

    public func quotedReplyAuthorColor() -> UIColor {
        quotedReplyTextColor()
    }

    public func quotedReplyTextColor() -> UIColor {
        isDarkThemeEnabled ? .ows_gray05 : .ows_gray90
    }

    public func quotedReplyAttachmentColor() -> UIColor {
        isDarkThemeEnabled ? .ows_gray05 : UIColor.ows_gray90
    }
}
