import SwiftUI

public extension AnyChangeEffect {
    /// An effect that wiggles the view when a change happens.
    static var wiggle: AnyChangeEffect {
        wiggle(rate: .default)
    }

    /// The rate of the wiggle effect.
    enum WiggleRate {
        case `default`
        case fast
        case phaseLength(CGFloat)

        fileprivate var phaseLength: CGFloat {
            switch self {
            case .default: return 0.8
            case .fast: return 0.3
            case .phaseLength(let phaseLength): return phaseLength
            }
        }
    }

    /// An effect that wiggles the view when a change happens.
    ///
    /// - Parameter rate: The rate of the wiggle.
    static func wiggle(rate: WiggleRate) -> AnyChangeEffect {
        .simulation({ change in
            WiggleSimulationModifier(impulseCount: change, phaseLength: rate.phaseLength)
        })
    }
}

internal struct WiggleSimulationModifier: ViewModifier, Simulative {
    // TODO: Not used, remove from protocol
    var initialVelocity: CGFloat = 0

    var impulseCount: Int

    var phaseLength: CGFloat

    @Environment(\.isConditionalEffect)
    private var isConditionalEffect

    @State
    private var wiggleCount: CGFloat = 0

    @State
    private var displacement: CGFloat = 0

    @State
    private var integrator: SecondOrderDynamics<CGFloat> = SecondOrderDynamics(
        f: 3,
        zeta: 0.85,
        r: -0.2
    )

    fileprivate var target: CGFloat {
        16 * sin(2 * .pi * wiggleCount)
    }

    private var isSimulationPaused: Bool {
        displacement == .zero && wiggleCount <= 0
    }

    public func body(content: Content) -> some View {
        let t = Transform3DEffect(
            angle: .degrees(displacement / 2),
            axis: (0, 0, 1)
        )

        TimelineView(.animation(paused: isSimulationPaused)) { context in
            content
                .modifier(t)
                .onChange(of: context.date) { (newValue: Date) in
                    let duration = Double(newValue.timeIntervalSince(context.date))
                    withAnimation(nil) {
                        update(max(0, min(duration, 1 / 30)))
                    }
                }
        }
        .onChange(of: impulseCount) { newValue in
            withAnimation(nil) {
                wiggleCount += 2

                if wiggleCount > 3 {
                    wiggleCount = 2 + fmod(wiggleCount, 1)
                }

                if isConditionalEffect {
                    wiggleCount += 4
                }
            }
        }
    }

    private func update(_ step: Double) {
        displacement = integrator.update(target: target, timestep: step)

        if !displacement.isNormal {
            displacement = 0
        }

        wiggleCount = clamp(0, wiggleCount - 2 * (step / phaseLength), .infinity)
    }
}


#if os(iOS) && DEBUG
struct WiggleEffect_Previews: PreviewProvider {
    struct Preview: View {
        @State
        var value: Int = 0

        var body: some View {
            VStack(spacing: 8) {
                Spacer()

                VStack(spacing: 32) {
                    Label("Answer", systemImage: "phone.fill")
                        .foregroundColor(.white)
                        .padding()
                        .background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
                        .changeEffect(.wiggle(rate: .fast), value: value)
                        .changeEffect(.shine(angle: .degrees(90), duration: 0.75), value: value)
                        .tint(.green)
                        .font(.largeTitle)
                }

                Spacer()

                Stepper(value: $value) {
                    Text("Value ") + Text("(\(value.formatted()))").foregroundColor(.secondary)
                }
            }
            .padding()
        }
    }

    struct Preview2: View {
        @State
        var isCalling: Bool = false

        var body: some View {
            VStack(spacing: 8) {
                Spacer()

                VStack(spacing: 32) {
                    Label("Answer", systemImage: "phone.fill")
                        .foregroundColor(.white)
                        .padding()
                        .background(.green, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
                        .conditionalEffect(.repeat(.wiggle(rate: .fast), every: 2), condition: isCalling)
                        .tint(.green)
                        .font(.largeTitle)
                }

                Spacer()

                Toggle("Calling", isOn: $isCalling)
            }
            .padding()
        }
    }

    static var previews: some View {
        Preview()
            .preferredColorScheme(.dark)
            .previewDisplayName("Change Effect")
        Preview2()
            .preferredColorScheme(.dark)
            .previewDisplayName("Conditional Effect")
    }
}
#endif
