//
//  FlipView.swift
//  Tusker
//
//  Created by Shadowfacts on 12/21/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import SwiftUI

// Based on https://stackoverflow.com/a/60807269
struct FlipView<Front: View, Back: View> : View {
    private let front: Front
    private let back: Back
    
    @State private var flipped = false
    @State private var angle: Angle = .zero
    @State private var width: CGFloat = 0
    @State private var initialFlipped: Bool!
    
    init(@ViewBuilder front: () -> Front, @ViewBuilder back: () -> Back) {
        self.front = front()
        self.back = back()
    }
    
    var body: some View {
        ZStack {
            front
                .opacity(flipped ? 0.0 : 1.0)
            back
                .rotation3DEffect(.degrees(180), axis: (0, 1, 0))
                .opacity(flipped ? 1.0 : 0.0)
        }
        .modifier(FlipEffect(flipped: $flipped, angle: angle.degrees, axis: (x: 0, y: 1)))
        .background(GeometryReader { proxy in
            Color.clear
                .preference(key: WidthPrefKey.self, value: proxy.size.width)
                .onPreferenceChange(WidthPrefKey.self) { newValue in
                    width = newValue
                }
        })
        .gesture(
            DragGesture()
                .onChanged({ value in
                    if initialFlipped == nil {
                        initialFlipped = flipped
                    }
                    let adj = (width / 2) - value.location.x
                    let hyp = abs((width / 2) - value.startLocation.x)
                    let clamped: Double = min(max(adj / hyp, -1), 1)
                    let startedOnRight = value.startLocation.x > width / 2
                    angle = .radians(acos(clamped) + (startedOnRight != initialFlipped ? .pi : 0))
                })
                .onEnded({ value in
                    initialFlipped = nil
                    let deg = angle.degrees.truncatingRemainder(dividingBy: 360)
                    if deg == 0 {
                        angle = .zero
                    } else if deg == 180 {
                        angle = .degrees(180)
                    } else {
                        withAnimation(.easeInOut(duration: 0.25)) {
                            angle = deg > 90 && deg < 270 ? .degrees(180) : .zero
                        }
                    }
                })
        )
        .onTapGesture {
            withAnimation(.easeInOut(duration: 0.5)) {
                angle = angle == .zero ? .degrees(180) : .zero
            }
        }
    }
}

// Based on https://swiftui-lab.com/swiftui-animations-part2/
struct FlipEffect: GeometryEffect {
    var animatableData: Double {
        get { angle }
        set { angle = newValue }
    }
    
    @Binding var flipped: Bool
    var angle: Double
    let axis: (x: CGFloat, y: CGFloat)
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        // We schedule the change to be done after the view has finished drawing,
        // otherwise, we would receive a runtime error, indicating we are changing
        // the state while the view is being drawn.
        DispatchQueue.main.async {
            self.flipped = self.angle >= 90 && self.angle < 270
        }
        
        let a = CGFloat(Angle.degrees(angle).radians)
        
        var transform3d = CATransform3DIdentity
        transform3d.m34 = -1/max(size.width, size.height)
        
        transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
        transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
        
        let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
        
        return ProjectionTransform(transform3d).concatenating(affineTransform)
    }
}

private struct WidthPrefKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

