import ComponentLibrary
import ComposableArchitecture
import SwiftUI
import Utilities

public typealias ToastType = ToastReducer.State.ToastType

private extension ToastReducer {
    static func makeTemporaryStore(dismiss: @escaping (() -> Void)) -> Store<State, Action> {
        Store(initialState: ToastReducer.State()) {
            Reduce<Self.State, Self.Action> { _, action in
                switch action {
                case .dismiss:
                    dismiss()
                    return .none

                case .setConnected, .setToast, .show, .undo:
                    return .none
                }
            }
            ToastReducer()
        }
    }
}

struct ToastModifier: ViewModifier {
    @State private var store: StoreOf<ToastReducer>
    private let toast: Binding<ToastType?>
    private let position: Toast.Position
    private let padding: EdgeInsets
    private let colorScheme: ColorScheme?

    private let action: ((ToastType.Destination?) -> Void)?
    private let undoAction: (() -> Void)?

    @Environment(\.colorScheme) var systemColorScheme

    init(
        toast: Binding<ToastType?>,
        position: Toast.Position,
        padding: EdgeInsets,
        colorScheme: ColorScheme? = nil,
        action: ((ToastType.Destination?) -> Void)? = nil,
        undoAction: (() -> Void)? = nil,
        dismiss: @escaping (() -> Void)
    ) {
        self.store = ToastReducer.makeTemporaryStore(dismiss: dismiss)
        self.toast = toast
        self.position = position
        self.padding = padding
        self.colorScheme = colorScheme
        self.action = action ?? { _ in toast.wrappedValue = nil }
        self.undoAction = undoAction
    }

    func body(content: Content) -> some View {
        content
            .overlay(alignment: position.alignment) {
                if store.toast != nil {
                    ToastView(
                        store: store,
                        action: action,
                        undoAction: undoAction
                    )
                    .padding(padding)
                    .colorScheme(colorScheme ?? systemColorScheme)
                }
            }
            .onAppear {
                if let toast = toast.wrappedValue {
                    store.send(.show(toast))
                }
            }
            .onChange(of: toast.wrappedValue) { _, newValue in
                if let newValue {
                    store.send(.show(newValue))
                } else {
                    store.send(.dismiss)
                }
            }
    }
}

private extension Toast.Position {
    var alignment: Alignment {
        switch self {
        case .bottom: return .bottom
        case .top: return .top
        }
    }
}

public extension View {
    /// Presents a toast message using the `ToastType` to produce the toast's content.
    ///
    /// For the toast to appear, the toast binding must not be nil. If the binding content changes, then the toast is re-presented.
    ///
    /// Use this function when you need to populate the fields of a toast with content from a data source.
    ///
    /// The example below shows how to use present a toast driven by a Reducer State:
    ///
    /// 1. First add `ToastType?` to your Reducer `State`:
    /// ```swift
    /// @Reducer
    /// struct MyReducer {
    ///     @ObservableState
    ///     struct State: Equatable {
    ///         var toastContent: ToastType?
    ///     }
    ///
    ///     var body: some ReducerOf<Self> {
    ///         Reducer { state, action in
    ///             switch action {
    ///             case let .setToast(toast):
    ///                 state.toastContent = toast
    ///             }
    ///         }
    ///     }
    /// }
    /// ```
    ///
    /// 2. Then in your View simply use the view modifier as such:
    /// ```swift
    /// struct MyView: View {
    ///     @Bindable let store: StoreOf<MyReducer>
    ///
    ///     var body: some ReducerOf<Self> {
    ///         someView
    ///             .toast($store.toastContent, position: .top)
    ///     }
    /// }
    /// ```
    ///
    /// If `ToastType` has its `autoDismiss` property set to `true` then the toast will automatically dismiss and `nil` the
    /// Binding's wrappedValue without any extra work. If however, `autoDismiss` is set to `false` and you want to manually
    /// dismiss the toast after eg. 2 seconds, then you can do the following:
    /// ```swift
    /// Effect.run { send in
    ///    try await Task.sleep(for: .seconds(2.0))
    ///    await send(.setToast(nil), animation: .spring())
    /// }
    /// ```
    ///
    /// - Parameters:
    ///   - toast: the toast's content and configuration
    ///   - position: the position to present the toast from
    ///   - padding: optional padding for the toast view
    ///   - colorScheme: optional color scheme to enforce on the toast
    ///   - action: optional action closure, called when the toast is tapped
    ///   - undoAction: optional undoAction, called when the undo button, if present,  is tapped
    @ViewBuilder func toast(
        _ toast: Binding<ToastType?>,
        position: Toast.Position,
        padding: EdgeInsets = .zero,
        colorScheme: ColorScheme? = nil,
        action: ((ToastType.Destination?) -> Void)? = nil,
        undoAction: (() -> Void)? = nil
    ) -> some View {
        modifier(
            ToastModifier(
                toast: toast,
                position: position,
                padding: padding,
                colorScheme: colorScheme,
                action: action,
                undoAction: undoAction
            ) {
                toast.wrappedValue = nil
            }
        )
    }
}
