//
//  GIFImageView.swift
//  TuskerComponents
//
//  Created by Shadowfacts on 11/11/21.
//  Copyright © 2021 Shadowfacts. All rights reserved.
//

import UIKit

open class GIFImageView: UIImageView {
    
    public fileprivate(set) var gifController: GIFController? = nil
    public var isAnimatingGIF: Bool { gifController?.state == .playing }
    
    /// Detaches the current GIF controller from this view.
    /// If this view is the GIF controller's only one, it will stop itself.
    public func detachGIFController() {
        gifController?.detach(from: self)
    }
    
}

/// A `GIFController` controls the animation of one or more `GIFImageView`s.
public class GIFController {
    
    // GIFImageView strongly holds the controller so that when the last view detaches, the controller is freed
    private var imageViews = WeakArray<GIFImageView>()
    
    public let gifData: Data
    private(set) var state: State = .stopped
    public private(set) var lastFrame: (image: UIImage, index: Int)? = nil
    
    public init(gifData: Data) {
        self.gifData = gifData
    }
    
    /// Attaches another view to this controller, letting it play back alongside the others.
    /// Immediately brings it into sync with the others, setting the last frame if there was one.
    public func attach(to view: GIFImageView) {
        imageViews.append(view)
        view.gifController = self
        
        if let lastFrame = lastFrame {
            view.image = lastFrame.image
        }
    }
    
    /// Detaches the given view from this controller.
    /// If no views attached views remain, the last strong reference to this controller is nilled out
    /// and image animation will stop at the next CGAnimateImageDataWithBlock callback.
    public func detach(from view: GIFImageView) {
        // todo: does === work the way i want here
        imageViews.removeAll(where: { $0 === view })
        view.gifController = nil
    }
    
    public func startAnimating() {
        guard state.shouldStop else { return }
        
        state = .playing
        
        CGAnimateImageDataWithBlock(gifData as CFData, nil) { [weak self] (frameIndex, cgImage, stop) in
            guard let self = self else {
                stop.pointee = true
                return
            }
            let image = UIImage(cgImage: cgImage)
            self.lastFrame = (image, frameIndex)
            for case let .some(view) in self.imageViews {
                view.image = image
            }
            stop.pointee = self.state.shouldStop
        }
    }
    
    public func stopAnimating() {
        guard state == .playing else { return }
        
        state = .stopping
    }
    
    enum State: Equatable {
        case stopped, playing, stopping
        
        var shouldStop: Bool {
            self == .stopped || self == .stopping
        }
    }
    
}

private class WeakHolder<T: AnyObject> {
    weak var object: T?
    
    init(_ object: T?) {
        self.object = object
    }
}

private struct WeakArray<Element: AnyObject>: MutableCollection, RangeReplaceableCollection {
    private var array: [WeakHolder<Element>]
    
    var startIndex: Int { array.startIndex }
    var endIndex: Int { array.endIndex }
    
    init() {
        array = []
    }
    
    init(_ elements: [Element]) {
        array = elements.map { WeakHolder($0) }
    }
    
    init(_ elements: [Element?]) {
        array = elements.map { WeakHolder($0) }
    }
    
    subscript(position: Int) -> Element? {
        get {
            array[position].object
        }
        set(newValue) {
            array[position] = WeakHolder(newValue)
        }
    }
    
    func index(after i: Int) -> Int {
        return array.index(after: i)
    }
    
    mutating func replaceSubrange<C>(_ subrange: Range<Int>, with newElements: C) where C : Collection, Self.Element == C.Element {
        array.replaceSubrange(subrange, with: newElements.map { WeakHolder($0) })
    }
}
