//
//  MatchedGeometryModifiers.swift
//  MatchGeom
//
//  Created by Shadowfacts on 4/24/23.
//

import SwiftUI

extension View {
    public func matchedGeometryPresentation<ID: Hashable, Presented: View>(id: Binding<ID?>, backgroundColor: UIColor, @ViewBuilder presenting: () -> Presented) -> some View {
        self.modifier(MatchedGeometryPresentationModifier(id: id, backgroundColor: backgroundColor, presented: presenting()))
    }
    
    public func matchedGeometrySource<ID: Hashable, ID2: Hashable>(id: ID, presentationID: ID2) -> some View {
        self.modifier(MatchedGeometrySourceModifier(id: AnyHashable(id), presentationID: AnyHashable(presentationID), matched: { AnyView(self) }))
    }
    
    public func matchedGeometryDestination<ID: Hashable>(id: ID) -> some View {
        self.modifier(MatchedGeometryDestinationModifier(id: AnyHashable(id), matched: self))
    }
}

private struct MatchedGeometryPresentationModifier<ID: Hashable, Presented: View>: ViewModifier {
    @Binding var id: ID?
    let backgroundColor: UIColor
    let presented: Presented
    @StateObject private var state = MatchedGeometryState()
    
    private var isPresented: Binding<Bool> {
        Binding {
            id != nil
        } set: {
            if $0 {
                fatalError()
            } else {
                id = nil
            }
        }
    }
    
    func body(content: Content) -> some View {
        content
            .environmentObject(state)
            .backgroundPreferenceValue(MatchedGeometrySourcesKey.self, { sources in
                Color.clear
                    .presentViewController(makeVC(allSources: sources), isPresented: isPresented)
            })
    }
    
    private func makeVC(allSources: [SourceKey: (AnyView, CGRect)]) -> () -> UIViewController {
        return {
            // force unwrap is safe, this closure is only called when being presented so we must have an id
            let id = AnyHashable(id!)
            return MatchedGeometryViewController(
                presentationID: id,
                content: presented,
                state: state,
                backgroundColor: backgroundColor
            )
        }
    }
}

private struct MatchedGeometrySourceModifier: ViewModifier {
    let id: AnyHashable
    let presentationID: AnyHashable
    let matched: () -> AnyView
    @EnvironmentObject private var state: MatchedGeometryState
    
    func body(content: Content) -> some View {
        content
            .background(GeometryReader { proxy in
                Color.clear
                    .preference(key: MatchedGeometryDestinationFrameKey.self, value: proxy.frame(in: .global))
                    .onPreferenceChange(MatchedGeometryDestinationFrameKey.self) { newValue in
                        if let newValue {
                            state.sources[SourceKey(presentationID: presentationID, matchedID: id)] = (matched, newValue)
                        }
                    }
            })
            .opacity(state.animating && state.presentationID == presentationID ? 0 : 1)
    }
}

private struct MatchedGeometryDestinationModifier<Matched: View>: ViewModifier {
    let id: AnyHashable
    let matched: Matched
    @EnvironmentObject private var state: MatchedGeometryState
    
    func body(content: Content) -> some View {
        content
            .background(GeometryReader { proxy in
                Color.clear
                    .preference(key: MatchedGeometryDestinationFrameKey.self, value: proxy.frame(in: .global))
                    .onPreferenceChange(MatchedGeometryDestinationFrameKey.self) { newValue in
                        if let newValue,
                           // ignore intermediate layouts that may happen while the dismiss animation is happening
                           state.mode != .dismissing {
                            state.destinations[id] = (AnyView(matched), newValue)
                        }
                    }
            })
            .opacity(state.animating ? 0 : 1)
    }
}

private struct MatchedGeometryDestinationFrameKey: PreferenceKey {
    static let defaultValue: CGRect? = nil
    static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) {
        value = nextValue()
    }
}

private struct MatchedGeometrySourcesKey: PreferenceKey {
    static let defaultValue: [SourceKey: (AnyView, CGRect)] = [:]
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value.merge(nextValue(), uniquingKeysWith: { _, new in new })
    }
}

struct SourceKey: Hashable {
    let presentationID: AnyHashable
    let matchedID: AnyHashable
}
