//
//  DiffableDataSource.Target.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 Foundation


// MARK: - DiffableDataSourc

extension DiffableDataSource {

    // MARK: - Target

    /**
     The `DiffableDataSource.Target` protocol allows custom views to consume `ListSnapshot` diffable data similar to how `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` reloads data for their corresponding views.
     */
    public typealias Target = DiffableDataSourceTarget
}


// MARK: - DiffableDataSource.Target

/**
 The `DiffableDataSource.Target` protocol allows custom views to consume `ListSnapshot` diffable data similar to how `DiffableDataSource.TableViewAdapter` and `DiffableDataSource.CollectionViewAdapter` reloads data for their corresponding views.
 */
public protocol DiffableDataSourceTarget: Sendable {

    // MARK: Public

    /**
     Whether `reloadData()` should be executed instead of `performBatchUpdates(updates:animated:)`.
     */
    var shouldSuspendBatchUpdates: Bool { get }

    /**
     Deletes one or more sections.
     */
    func deleteSections(at indices: IndexSet, animated: Bool)

    /**
     Inserts one or more sections
     */
    func insertSections(at indices: IndexSet, animated: Bool)

    /**
     Reloads the specified sections.
     */
    func reloadSections(at indices: IndexSet, animated: Bool)

    /**
     Moves a section to a new location.
     */
    func moveSection(at index: IndexSet.Element, to newIndex: IndexSet.Element, animated: Bool)

    /**
     Deletes the items specified by an array of index paths.
     */
    func deleteItems(at indexPaths: [IndexPath], animated: Bool)

    /**
     Inserts items at the locations identified by an array of index paths.
     */
    func insertItems(at indexPaths: [IndexPath], animated: Bool)

    /**
     Reloads the specified items.
     */
    func reloadItems(at indexPaths: [IndexPath], animated: Bool)

    /**
     Moves the item at a specified location to a destination location.
     */
    func moveItem(at indexPath: IndexPath, to newIndexPath: IndexPath, animated: Bool)

    /**
     Animates multiple insert, delete, reload, and move operations as a group.
     */
    func performBatchUpdates(updates: @escaping @Sendable () -> Void, animated: Bool, completion: @escaping @Sendable () -> Void)

    /**
     Reloads all sections and items.
     */
    func reloadData()
}

extension DiffableDataSource.Target {

    // MARK: Internal

    internal func reload<C: Sendable, O>(
        using stagedChangeset: Internals.DiffableDataUIDispatcher<O>.StagedChangeset<C>,
        animated: Bool,
        interrupt: ((Internals.DiffableDataUIDispatcher<O>.Changeset<C>) -> Bool)? = nil,
        setData: @escaping @Sendable (C) -> Void,
        completion: @escaping () -> Void
    ) {

        let group = DispatchGroup()
        defer {

            group.notify(
                queue: .main,
                execute: {
                    
                    completion()
                }
            )
        }
        if self.shouldSuspendBatchUpdates, let data = stagedChangeset.last?.data {

            setData(data)
            self.reloadData()
            return
        }
        for changeset in stagedChangeset {

            if let interrupt = interrupt,
                interrupt(changeset),
                let data = stagedChangeset.last?.data {

                setData(data)
                self.reloadData()
                return
            }
            group.enter()
            self.performBatchUpdates(
                updates: {

                    setData(changeset.data)

                    if !changeset.sectionDeleted.isEmpty {

                        self.deleteSections(
                            at: IndexSet(changeset.sectionDeleted),
                            animated: animated
                        )
                    }
                    if !changeset.sectionInserted.isEmpty {

                        self.insertSections(
                            at: IndexSet(changeset.sectionInserted),
                            animated: animated
                        )
                    }
                    if !changeset.sectionUpdated.isEmpty {

                        self.reloadSections(
                            at: IndexSet(changeset.sectionUpdated),
                            animated: animated
                        )
                    }
                    for (source, target) in changeset.sectionMoved {

                        self.moveSection(
                            at: source,
                            to: target,
                            animated: animated
                        )
                    }
                    if !changeset.elementDeleted.isEmpty {

                        self.deleteItems(
                            at: changeset.elementDeleted.map {

                                IndexPath(item: $0.element, section: $0.section)
                            },
                            animated: animated
                        )
                    }
                    if !changeset.elementInserted.isEmpty {

                        self.insertItems(
                            at: changeset.elementInserted.map {

                                IndexPath(item: $0.element, section: $0.section)
                            },
                            animated: animated
                        )
                    }
                    if !changeset.elementUpdated.isEmpty {

                        self.reloadItems(
                            at: changeset.elementUpdated.map {

                                IndexPath(item: $0.element, section: $0.section)
                            },
                            animated: animated
                        )
                    }
                    for (source, target) in changeset.elementMoved {

                        self.moveItem(
                            at: IndexPath(item: source.element, section: source.section),
                            to: IndexPath(item: target.element, section: target.section),
                            animated: animated
                        )
                    }
                },
                animated: animated,
                completion: {
                    
                    group.leave()
                }
            )
        }
    }
}

#endif
