import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import Photos
import LegacyComponents
import AttachmentUI
import ItemListUI
import CameraScreen

private enum MediaGroupsEntry: Comparable, Identifiable {
    enum StableId: Hashable {
        case albumsHeader
        case albums
        case smartAlbumsHeader
        case smartAlbum(String)
    }
    
    case albumsHeader(PresentationTheme, String)
    case albums(PresentationTheme, [PHAssetCollection])
    case smartAlbumsHeader(PresentationTheme, String)
    case smartAlbum(PresentationTheme, Int, PHAssetCollection, Int)
        
    var stableId: StableId {
        switch self {
            case .albumsHeader:
                return .albumsHeader
            case .albums:
                return .albums
            case .smartAlbumsHeader:
                return .smartAlbumsHeader
            case let .smartAlbum(_, _, album, _):
                return .smartAlbum(album.localIdentifier)
        }
    }

    static func ==(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
        switch lhs {
            case let .albumsHeader(lhsTheme, lhsText):
                if case let .albumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
                    return true
                } else {
                    return false
                }
            case let .albums(lhsTheme, lhsAssetCollections):
                if case let .albums(rhsTheme, rhsAssetCollections) = rhs, lhsTheme === rhsTheme, lhsAssetCollections == rhsAssetCollections {
                    return true
                } else {
                    return false
                }
            case let .smartAlbumsHeader(lhsTheme, lhsText):
                if case let .smartAlbumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
                    return true
                } else {
                    return false
                }
            case let .smartAlbum(lhsTheme, lhsIndex, lhsAssetCollection, lhsCount):
                if case let .smartAlbum(rhsTheme, rhsIndex, rhsAssetCollection, rhsCount) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsAssetCollection == rhsAssetCollection, lhsCount == rhsCount {
                    return true
                } else {
                    return false
                }
        }
    }
    
    private var sortId: Int {
        switch self {
        case .albumsHeader:
            return 0
        case .albums:
            return 1
        case .smartAlbumsHeader:
            return 2
        case let .smartAlbum(_, index, _, _):
            return 3 + index
        }
    }
    
    static func <(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
        return lhs.sortId < rhs.sortId
    }
    
    func item(presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> ListViewItem {
        switch self {
            case let .albumsHeader(_, text), let .smartAlbumsHeader(_, text):
                return MediaGroupsHeaderItem(presentationData: ItemListPresentationData(presentationData), title: text)
            case let .albums(_, collections):
                return MediaGroupsAlbumGridItem(presentationData: presentationData, collections: collections, action: { collection in
                    openGroup(collection)
                })
            case let .smartAlbum(_, _, collection, count):
                let title = collection.localizedTitle ?? ""
                
                let count = presentationStringsFormattedNumber(Int32(count), presentationData.dateTimeFormat.groupingSeparator)
                var icon: MediaGroupsAlbumItem.Icon?
                switch  collection.assetCollectionSubtype {
                    case .smartAlbumAnimated:
                        icon = .animated
                    case .smartAlbumBursts:
                        icon = .bursts
                    case .smartAlbumDepthEffect:
                        icon = .depthEffect
                    case .smartAlbumLivePhotos:
                        icon = .livePhotos
                    case .smartAlbumPanoramas:
                        icon = .panoramas
                    case .smartAlbumScreenshots:
                        icon = .screenshots
                    case .smartAlbumSelfPortraits:
                        icon = .selfPortraits
                    case .smartAlbumSlomoVideos:
                        icon = .slomoVideos
                    case .smartAlbumTimelapses:
                        icon = .timelapses
                    case .smartAlbumVideos:
                        icon = .videos
                    case .smartAlbumAllHidden:
                        icon = .hidden
                    default:
                        icon = nil
                }
                return MediaGroupsAlbumItem(presentationData: ItemListPresentationData(presentationData), title: title, count: count, icon: icon, action: {
                    openGroup(collection)
                })
        }
    }
}

private struct MediaGroupsTransition {
    let deletions: [ListViewDeleteItem]
    let insertions: [ListViewInsertItem]
    let updates: [ListViewUpdateItem]
}

private func preparedTransition(from fromEntries: [MediaGroupsEntry], to toEntries: [MediaGroupsEntry], presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> MediaGroupsTransition {
    let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
    
    let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
    let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
    let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
    
    return MediaGroupsTransition(deletions: deletions, insertions: insertions, updates: updates)
}

public final class MediaGroupsScreen: ViewController, AttachmentContainable {
    public var requestAttachmentMenuExpansion: () -> Void = {}
    public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
    public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
    public var cancelPanGesture: () -> Void = { }
    public var isContainerPanning: () -> Bool = { return false }
    public var isContainerExpanded: () -> Bool = { return false }
    
    public var mediaPickerContext: AttachmentMediaPickerContext? {
        return nil
    }
    
    private let context: AccountContext
    private var presentationData: PresentationData
    private var presentationDataDisposable: Disposable?
    private let mediaAssetsContext: MediaAssetsContext
    private let embedded: Bool
    private let openGroup: (PHAssetCollection) -> Void
    
    private class Node: ViewControllerTracingNode {
        struct State {
            let albums: PHFetchResult<PHAssetCollection>
            let smartAlbums: PHFetchResult<PHAssetCollection>
        }
        
        private weak var controller: MediaGroupsScreen?
        private var presentationData: PresentationData
        
        private let containerNode: ASDisplayNode
        private let backgroundNode: NavigationBackgroundNode
        private let listNode: ListView
    
        private var nextStableId: Int = 1
        private var currentEntries: [MediaGroupsEntry] = []
        private var enqueuedTransactions: [MediaGroupsTransition] = []
        private var state: State?
        
        private var itemsDisposable: Disposable?
        
        private var didSetReady = false
        private let _ready = Promise<Bool>()
        var ready: Promise<Bool> {
            return self._ready
        }
        
        private var validLayout: (ContainerViewLayout, CGFloat)?
        
        init(controller: MediaGroupsScreen) {
            self.controller = controller
            self.presentationData = controller.presentationData

            self.containerNode = ASDisplayNode()
            self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
            
            self.listNode = ListView()

            super.init()
            
            if !controller.embedded {
                self.addSubnode(self.backgroundNode)
            } else {
                self.containerNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
            }
            self.addSubnode(self.containerNode)
            self.containerNode.addSubnode(self.listNode)
                        
            let updatedState = combineLatest(queue: Queue.mainQueue(), controller.mediaAssetsContext.fetchAssetsCollections(.album), controller.mediaAssetsContext.fetchAssetsCollections(.smartAlbum))
            self.itemsDisposable = (updatedState
            |> deliverOnMainQueue).start(next: { [weak self] albums, smartAlbums in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.updateState(State(albums: albums, smartAlbums: smartAlbums))
            })
            
            self.listNode.beganInteractiveDragging = { [weak self] _ in
                self?.view.window?.endEditing(true)
            }
            
            self.listNode.visibleContentOffsetChanged = { [weak self] _ in
                self?.updateNavigation(transition: .immediate)
            }
        }
        
        deinit {
            self.itemsDisposable?.dispose()
        }
        
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            let result = super.hitTest(point, with: event)
            if result == self.view {
                return nil
            }
            return result
        }
                        
        private func updateState(_ state: State) {
            self.state = state
            
            var entries: [MediaGroupsEntry] = []
                       
            var albums: [PHAssetCollection] = []
            entries.append(.albumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MyAlbums))
            state.smartAlbums.enumerateObjects { collection, _, _ in
                if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
                    albums.append(collection)
                }
            }
            state.albums.enumerateObjects(options: [.reverse]) { collection, _, _ in
                albums.append(collection)
            }
            entries.append(.albums(self.presentationData.theme, albums))
            
            let smartAlbumsHeaderIndex = entries.count
            
            var addedSmartAlbum = false
            state.smartAlbums.enumerateObjects { collection, index, _ in
                var supportedAlbums: [PHAssetCollectionSubtype] = [
                    .smartAlbumBursts,
                    .smartAlbumPanoramas,
                    .smartAlbumScreenshots,
                    .smartAlbumSelfPortraits,
                    .smartAlbumSlomoVideos,
                    .smartAlbumTimelapses,
                    .smartAlbumVideos,
                    .smartAlbumAllHidden
                ]
                if #available(iOS 11, *) {
                    supportedAlbums.append(.smartAlbumAnimated)
                    supportedAlbums.append(.smartAlbumDepthEffect)
                    supportedAlbums.append(.smartAlbumLivePhotos)
                }
                if supportedAlbums.contains(collection.assetCollectionSubtype) {
                    let result = PHAsset.fetchAssets(in: collection, options: nil)
                    if result.count > 0 {
                        addedSmartAlbum = true
                        entries.append(.smartAlbum(self.presentationData.theme, index, collection, result.count))
                    }
                }
            }
            if addedSmartAlbum {
                entries.insert(.smartAlbumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MediaTypes), at: smartAlbumsHeaderIndex)
            }
            
            let previousEntries = self.currentEntries
            self.currentEntries = entries
            
            let transaction = preparedTransition(from: previousEntries, to: entries, presentationData: self.presentationData, openGroup: { [weak self] collection in
                self?.view.window?.endEditing(true)
                self?.controller?.openGroup(collection)
            })
            self.enqueueTransaction(transaction)
        }

        
        func updatePresentationData(_ presentationData: PresentationData) {
            self.presentationData = presentationData
            
            if self.controller?.embedded == true {
                self.containerNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
            }
            self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
        }
        
        private func enqueueTransaction(_ transaction: MediaGroupsTransition) {
            self.enqueuedTransactions.append(transaction)
            
            if let _ = self.validLayout {
                while !self.enqueuedTransactions.isEmpty {
                    self.dequeueTransaction()
                }
            }
        }
        
        private func dequeueTransaction() {
            if self.enqueuedTransactions.isEmpty {
                return
            }
            let transaction = self.enqueuedTransactions.removeFirst()
            
            let options = ListViewDeleteAndInsertOptions()
            
            self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
                if let strongSelf = self {
                    if !strongSelf.didSetReady {
                        strongSelf.didSetReady = true
                        strongSelf._ready.set(.single(true))
                    }
                }
            })
 
        }
        
        func scrollToTop() {
            self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
        }
        
        func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
            guard let controller = self.controller else {
                return
            }
            let firstTime = self.validLayout == nil
            self.validLayout = (layout, navigationBarHeight)
            
            let topInset: CGFloat = controller.embedded ? 12.0 : 0.0
            
            let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + topInset), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - topInset))
            transition.updateFrame(node: self.containerNode, frame: containerFrame)
            
            transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: layout.size))
            self.backgroundNode.update(size: layout.size, transition: transition)
            
            let size = layout.size
            let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
            self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + 56.0, right: layout.safeInsets.right), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
            transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
            
            if firstTime {
                self.dequeueTransaction()
            }
        }
        
        private var previousContentOffset: GridNodeVisibleContentOffset?
        func updateNavigation(delayDisappear: Bool = false, transition: ContainedViewLayoutTransition) {
            var previousContentOffsetValue: CGFloat?
            if let previousContentOffset = self.previousContentOffset, case let .known(value) = previousContentOffset {
                previousContentOffsetValue = value
            }
            
            let offset = self.listNode.visibleContentOffset()
            switch offset {
            case let .known(value):
                let transition: ContainedViewLayoutTransition
                if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 2.0 {
                    transition = .animated(duration: 0.2, curve: .easeInOut)
                } else {
                    transition = .immediate
                }
                self.controller?.navigationBar?.updateBackgroundAlpha(min(2.0, value) / 2.0, transition: transition)
            case .unknown, .none:
                self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
            }
        }
    }
    
    private var validLayout: ContainerViewLayout?
    
    private var controllerNode: Node {
        return self.displayNode as! Node
    }
    
    private let _ready = Promise<Bool>()
    override public var ready: Promise<Bool> {
        return self._ready
    }
    
    init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mediaAssetsContext: MediaAssetsContext, embedded: Bool = false, openGroup: @escaping (PHAssetCollection) -> Void) {
        self.context = context
        self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
        self.mediaAssetsContext = mediaAssetsContext
        self.embedded = embedded
        self.openGroup = openGroup
        
        super.init(navigationBarPresentationData: !embedded ? NavigationBarPresentationData(presentationData: presentationData) : nil)
                
        self.statusBar.statusBarStyle = .Ignore
        
        self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
        |> deliverOnMainQueue).start(next: { [weak self] presentationData in
            if let strongSelf = self {
                let previousTheme = strongSelf.presentationData.theme
                let previousStrings = strongSelf.presentationData.strings
                
                strongSelf.presentationData = presentationData
                                
                if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
                    strongSelf.controllerNode.updatePresentationData(presentationData)
                }
            }
        })
        
        self.scrollToTop = { [weak self] in
            if let strongSelf = self {
                strongSelf.controllerNode.scrollToTop()
            }
        }
        
        if !embedded {
            self.title = "Albums"
            
            self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed))
        }
    }
    
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        self.presentationDataDisposable?.dispose()
    }
    
    override public func loadDisplayNode() {
        self.displayNode = Node(controller: self)
        
        self._ready.set(self.controllerNode.ready.get())
        
        super.displayNodeDidLoad()
    }
    
    @objc private func backPressed() {
        if let _ = self.navigationController {
            self.dismiss()
        } else {
            self.updateNavigationStack { current in
                var mediaPickerContext: AttachmentMediaPickerContext?
                if let first = current.first as? MediaPickerScreen {
                    mediaPickerContext = first.webSearchController?.mediaPickerContext ?? first.mediaPickerContext
                }
                return (current.filter { $0 !== self }, mediaPickerContext)
            }
        }
    }
            
    override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
        super.containerLayoutUpdated(layout, transition: transition)
        
        self.validLayout = layout
        self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
    }
}