import SwiftUI

/// The button style for the controls of a ``RecordingView``.
///
/// The style renders the button's label, filled with the background color of the label's color scheme, over a padded, circular red Liquid Glass
/// background that reacts fluidly to presses, and dims the button while disabled. Both the label and its padding scale relative to the current
/// dynamic type size.
///
/// By default, the label's color scheme is inverted from the environment's, so its fill contrasts with the surroundings; this behavior can be turned off
/// through the ``init(invertStyle:)`` initializer.
struct RecordingButtonStyle: ButtonStyle {

    // MARK: Properties

    /// The color scheme of the environment.
    @Environment(\.colorScheme) private var colorScheme

    /// Whether the button allows user interaction.
    @Environment(\.isEnabled) private var isEnabled
    
    /// The padding between the button's label and its background, scaled relative to the current dynamic type size.
    @ScaledMetric private var padding = Constant.Padding.button

    /// The width and height of the button's label, scaled relative to the current dynamic type size.
    @ScaledMetric private var size = Constant.Size.button
    
    /// Whether the color scheme of the button's label should be inverted from the environment's.
    private let invertStyle: Bool

    // MARK: Initializers

    /// Creates a recording button style.
    ///
    /// - Parameter invertStyle: Whether the foreground style of the button's label should be inverted from the environment's.
    init(
        invertStyle: Bool = false
    ) {
        self.invertStyle = invertStyle
    }

    // MARK: Methods

    /// Creates the styled body of the button.
    ///
    /// - Parameter configuration: The properties of the button being styled.
    func makeBody(
        configuration: Configuration
    ) -> some View {
        configuration.label
            .frame(
                width: size,
                height: size
            )
            .padding(padding)
            .scaledToFit()
            .foregroundStyle(.windowBackground)
            .environment(
                \.colorScheme,
                 colorSchemeLabel
            )
            .glassEffect(
                .regular
                    .tint(.red)
                    .interactive(),
                in: .circle
            )
            .opacity(opacity)
    }

}

// MARK: - Helpers

private extension RecordingButtonStyle {
    
    // MARK: Computed
    
    /// The color scheme for the button's label: the opposite of the environment's when ``invertStyle`` is set, the environment's otherwise.
    var colorSchemeLabel: ColorScheme {
        guard invertStyle else {
            return colorScheme
        }

        return colorScheme == .dark
            ? .light
            : .dark
    }

    /// The opacity of the button: dimmed while disabled, fully opaque otherwise.
    var opacity: Double {
        isEnabled
            ? Constant.Opacity.enabled
            : Constant.Opacity.disabled
    }

}

// MARK: - Styles

extension ButtonStyle where Self == RecordingButtonStyle {

    /// The button style for the controls of a ``RecordingView``, inverting the color scheme of the button's label.
    static var recording: RecordingButtonStyle {
        .init()
    }

    /// The button style for the controls of a ``RecordingView``.
    ///
    /// - Parameter invertStyle: Whether the color scheme of the button's label should be inverted from the environment's.
    /// - Returns: A recording button style with the given color scheme behavior.
    static func recording(
        invertStyle: Bool
    ) -> RecordingButtonStyle {
        .init(invertStyle: invertStyle)
    }

}

// MARK: - Constants

/// The constant values used across the button style.
private enum Constant {
    /// The opacity constants.
    enum Opacity {
        /// The opacity of a disabled button.
        static let disabled: Double = 0.5
        /// The opacity of an enabled button.
        static let enabled: Double = 1
    }

    /// The padding constants.
    enum Padding {
        /// The base padding between a button's label and its background, before dynamic type scaling.
        static let button: CGFloat = 8
    }

    /// The size constants.
    enum Size {
        /// The base width and height of a button's label, before dynamic type scaling.
        static let button: CGFloat = 32
    }
}

// MARK: - Previews

#Preview(
    "Recording button style"
) {
    @Previewable @State var isDisabled: Bool = false
    @Previewable @State var isStyleInverted: Bool = false
    
    Button {
        // Button action closure.
    } label: {
        Image(.Icon.record)
            .resizable()
    }
    .buttonStyle(.recording(
        invertStyle: isStyleInverted
    ))
    .disabled(isDisabled)
    .safeAreaInset(edge: .bottom) {
        VStack {
            Toggle(
                "Style inverted",
                isOn: $isStyleInverted
            )

            Toggle(
                "Button disabled",
                isOn: $isDisabled
            )
        }
        .fontWeight(.semibold)
        .dynamicTypeSize(.large)
        .padding()
        .tint(.red)
    }
}
