// Author: The SwiftUI-Lab
// This code is part of the tutorial: https://swiftui-lab.com/swiftui-animations-part4/
import SwiftUI

// Sample usage
struct ContentView: View {
    var body: some View {
        VStack {
            GifImage(url: URL(string: "https://media.giphy.com/media/YAlhwn67KT76E/giphy.gif?cid=790b7611b26260b2ad23535a70e343e67443ff80ef623844&rid=giphy.gif&ct=g")!)
                .padding(10)
                .overlay {
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(.green)
                }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

// ObservableObject that holds the data and logic to get all frames in the gif image.
class GifData: ObservableObject {
    var loopCount: Int = 0
    var width: CGFloat = 0
    var height: CGFloat = 0
    
    var capInsets: EdgeInsets?
    var resizingMode: Image.ResizingMode
    
    struct ImageFrame {
        let image: Image
        let delay: TimeInterval
    }
    
    var frames: [ImageFrame] = []
    
    init(url: URL, capInsets: EdgeInsets?, resizingMode: Image.ResizingMode) {
        self.capInsets = capInsets
        self.resizingMode = resizingMode
        
        let label = url.deletingPathExtension().lastPathComponent

        Task {
            guard let (data, _) = try? await URLSession.shared.data(from: url) else { return }

            guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return }
            
            let imageCount = CGImageSourceGetCount(source)
            
            guard let imgProperties = CGImageSourceCopyProperties(source, nil) as? Dictionary<String, Any> else { return }
            
            guard let gifProperties = imgProperties[kCGImagePropertyGIFDictionary as String] as? Dictionary<String, Any> else { return }
            
            loopCount = gifProperties[kCGImagePropertyGIFLoopCount as String] as? Int ?? 0
            width = gifProperties[kCGImagePropertyGIFCanvasPixelWidth as String] as? CGFloat ?? 0
            height = gifProperties[kCGImagePropertyGIFCanvasPixelHeight as String] as? CGFloat ?? 0
            
            let frameInfo = gifProperties[kCGImagePropertyGIFFrameInfoArray as String] as? [Dictionary<String, TimeInterval>] ?? []
            
            for i in 0 ..< min(imageCount, frameInfo.count) {
                if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
                    
                    var img = Image(image, scale: 1.0, label: Text(label))
                    
                    if let insets = capInsets {
                        img = img.resizable(capInsets: insets, resizingMode: resizingMode)
                    }
                    
                    frames.append(
                        ImageFrame(image: img,
                                   delay: frameInfo[i][kCGImagePropertyGIFDelayTime as String] ?? 0.05)
                    )
                }
            }
            
            DispatchQueue.main.async { self.objectWillChange.send() }
        }
    }
}

// The GifImage view
struct GifImage: View {
    @StateObject var gifData: GifData
    
    /// Create an animated Gif Image
    /// - Parameters:
    ///   - url: the url holding the animated gif file
    ///   - capInsets: if nil, image is not resizable. Otherwise, the capInsets for image resizing (same as the standard image .resizable() modifier).
    ///   - resizingMode: ignored if capInsets is nil, otherwise, equivalent to the standard image .resizable() modifier parameter)
    init(url: URL, capInsets: EdgeInsets? = nil, resizingMode: Image.ResizingMode = .stretch) {
        _gifData = StateObject(wrappedValue: GifData(url: url, capInsets: capInsets, resizingMode: resizingMode))
    }
    
    var body: some View {
        Group {
            if gifData.frames.count == 0 {
                Color.clear
            } else {
                VStack {
                    TimelineView(.cyclic(loopCount: gifData.loopCount, timeOffsets: gifData.frames.map { $0.delay })) { timeline in
                        ImageFrame(gifData: gifData, date: timeline.date)
                    }
                }
            }
        }
    }
    
    struct ImageFrame: View {
        @State private var frame = 0
        let gifData: GifData
        let date: Date
        
        var body: some View {
            gifData.frames[frame].image
                .onChange(of: date) { _ in
                    frame = (frame + 1) % gifData.frames.count
                }
        }
    }
}

// A cyclic TimelineSchedule
struct CyclicTimelineSchedule: TimelineSchedule {
    let loopCount: Int // loopCount == 0 means inifinite loops.
    let timeOffsets: [TimeInterval]
    
    func entries(from startDate: Date, mode: TimelineScheduleMode) -> Entries {
        Entries(loopCount: loopCount, last: startDate, offsets: timeOffsets)
    }
    
    struct Entries: Sequence, IteratorProtocol {
        let loopCount: Int
        var loops = 0
        var last: Date
        let offsets: [TimeInterval]
        
        var idx: Int = -1
        
        mutating func next() -> Date? {
            idx = (idx + 1) % offsets.count
            
            if idx == 0 { loops += 1 }
            
            if loopCount != 0 && loops >= loopCount { return nil }
            
            last = last.addingTimeInterval(offsets[idx])
            
            return last
        }
    }
}

extension TimelineSchedule where Self == CyclicTimelineSchedule {
    static func cyclic(loopCount: Int, timeOffsets: [TimeInterval]) -> CyclicTimelineSchedule {
        .init(loopCount: loopCount, timeOffsets: timeOffsets)
    }
}