//
//  Internals.DiffableDataUIDispatcher.swift
//  CoreStore
//
//  Copyright © 2018 John Rommel Estropia
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

#if canImport(UIKit) || canImport(AppKit)

import CoreData

#if canImport(QuartzCore)
import QuartzCore

#endif


// MARK: - Internals

extension Internals {

    // MARK: Internal

    // Implementation based on https://github.com/ra1028/DiffableDataSources
    @usableFromInline
    internal final class DiffableDataUIDispatcher<O: DynamicObject>: @unchecked Sendable {

        // MARK: Internal
        
        typealias ObjectType = O
        
        init(dataStack: DataStack) {
            
            self.dataStack = dataStack
        }

        func purge<Target: DiffableDataSource.Target>(
            target: Target?,
            animatingDifferences: Bool,
            performUpdates: @escaping @Sendable (
                Target,
                StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
                @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
            ) -> Void
        ) {

            self.apply(
                .init(),
                target: target,
                animatingDifferences: animatingDifferences,
                performUpdates: performUpdates
            )
        }
        
        func apply<Target: DiffableDataSource.Target>(
            _ snapshot: DiffableDataSourceSnapshot,
            target: Target?,
            animatingDifferences: Bool,
            performUpdates: @escaping @Sendable (
                Target,
                StagedChangeset<[Internals.DiffableDataSourceSnapshot.Section]>,
                @escaping @Sendable ([Internals.DiffableDataSourceSnapshot.Section]) -> Void
            ) -> Void
        ) {
            
            self.dispatcher.dispatch { @MainActor [weak self] in
                
                guard let self = self else {
                    
                    return
                }

                self.currentSnapshot = snapshot

                let newSections = snapshot.sections
                guard let target = target else {
                    
                    self.sections = newSections
                    return
                }

                let performDiffingUpdates: @MainActor @Sendable () -> Void = {
                    
                    let changeset = StagedChangeset(source: self.sections, target: newSections)
                    performUpdates(
                        target,
                        changeset,
                        { sections in
                            
                            self.sections = sections
                        }
                    )
                }

#if os(watchOS) || !canImport(QuartzCore)

                performDiffingUpdates()

#else

                CATransaction.begin()
                
                if !animatingDifferences {
                    
                    CATransaction.setDisableActions(true)
                }
                performDiffingUpdates()
                
                CATransaction.commit()

#endif
            }
        }

        func snapshot() -> DiffableDataSourceSnapshot {
            
            var snapshot: DiffableDataSourceSnapshot = .init()
            snapshot.sections = self.currentSnapshot.sections
            return snapshot
        }

        func sectionIdentifier(inSection section: Int) -> String? {

            guard self.sections.indices.contains(section) else {

                return nil
            }
            return self.sections[section].differenceIdentifier
        }

        func itemIdentifier(for indexPath: IndexPath) -> NSManagedObjectID? {

            guard self.sections.indices.contains(indexPath.section) else {
                
                return nil
            }
            let items = self.sections[indexPath.section].elements
            guard items.indices.contains(indexPath.item) else {
                
                return nil
            }
            return items[indexPath.item].differenceIdentifier
        }

        func indexPath(for itemIdentifier: NSManagedObjectID) -> IndexPath? {
            
            let indexPathMap: [NSManagedObjectID: IndexPath] = self.sections.enumerated().reduce(into: [:]) { result, section in
                
                for (itemIndex, item) in section.element.elements.enumerated() {
                    
                    result[item.differenceIdentifier] = IndexPath(
                        item: itemIndex,
                        section: section.offset
                    )
                }
            }
            return indexPathMap[itemIdentifier]
        }

        func numberOfSections() -> Int {
            
            return self.sections.count
        }

        func numberOfItems(inSection section: Int) -> Int? {

            guard self.sections.indices.contains(section) else {

                return nil
            }
            return self.sections[section].elements.count
        }
        
        func sectionIndexTitle(for section: Int) -> String? {
            
            guard self.sections.indices.contains(section) else {

                return nil
            }
            return self.sections[section].indexTitle
        }
        
        func sectionIndexTitlesForAllSections() -> [String?] {
            
            return self.sections.map({ $0.indexTitle })
        }


        // MARK: Private

        private let dispatcher: MainThreadSerialDispatcher = .init()
        private let dataStack: DataStack

        private var currentSnapshot: Internals.DiffableDataSourceSnapshot = .init()
        private var sections: [Internals.DiffableDataSourceSnapshot.Section] = []
        
        
        // MARK: - ElementPath
        
        @usableFromInline
        internal struct ElementPath: Hashable, Sendable {
            
            @usableFromInline
            var element: Int
            
            @usableFromInline
            var section: Int

            @usableFromInline
            init(element: Int, section: Int) {
                
                self.element = element
                self.section = section
            }
        }


        // MARK: - MainThreadSerialDispatcher

        fileprivate final class MainThreadSerialDispatcher: Sendable {

            // MARK: FilePrivate

            fileprivate init() {}

            fileprivate func dispatch(_ action: @escaping @MainActor @Sendable () -> Void) {

                let count = self.executingCount.incrementAndGet()
                if Thread.isMainThread && count == 1 {

                    MainActor.assumeIsolated {
                        
                        action()
                    }
                    self.executingCount.decrement()
                }
                else {

                    DispatchQueue.main.async { [weak self] in

                        guard let self = self else {

                            return
                        }
                        action()
                        self.executingCount.decrement()
                    }
                }
            }


            // MARK: Private

            private let executingCount: AtomicInt = .init()

            
            // MARK: - AtomicInt
            
            fileprivate final class AtomicInt: Sendable {
                
                // MARK: FilePrivate

                fileprivate func incrementAndGet() -> Int {

                    return self.value.withLock {
                        
                        $0 += 1
                        return $0
                    }
                }

                fileprivate func decrement() {

                    self.value.withLock {
                        
                        $0 -= 1
                    }
                }

                
                // MARK: Private

                private let value: Internals.Mutex<Int> = .init(0)
            }
        }
        
    }
}

#endif
