import Foundation
#if os(macOS)
    import PostboxMac
    import SwiftSignalKitMac
#else
    import Postbox
    import SwiftSignalKit
#endif

import SyncCore

public struct HistoryPreloadIndex: Comparable {
    public let index: ChatListIndex?
    public let hasUnread: Bool
    public let isMuted: Bool
    public let isPriority: Bool
    
    public init(index: ChatListIndex?, hasUnread: Bool, isMuted: Bool, isPriority: Bool) {
        self.index = index
        self.hasUnread = hasUnread
        self.isMuted = isMuted
        self.isPriority = isPriority
    }
    
    public static func <(lhs: HistoryPreloadIndex, rhs: HistoryPreloadIndex) -> Bool {
        if lhs.isPriority != rhs.isPriority {
            if lhs.isPriority {
                return true
            } else {
                return false
            }
        }
        if lhs.isMuted != rhs.isMuted {
            if lhs.isMuted {
                return false
            } else {
                return true
            }
        }
        if lhs.hasUnread != rhs.hasUnread {
            if lhs.hasUnread {
                return true
            } else {
                return false
            }
        }
        if let lhsIndex = lhs.index, let rhsIndex = rhs.index {
            return lhsIndex > rhsIndex
        } else if lhs.index != nil {
            return true
        } else if rhs.index != nil {
            return false
        } else {
            return true
        }
    }
}

private struct HistoryPreloadHole: Hashable, Comparable {
    let preloadIndex: HistoryPreloadIndex
    let hole: MessageOfInterestHole
    
    static func ==(lhs: HistoryPreloadHole, rhs: HistoryPreloadHole) -> Bool {
        return lhs.preloadIndex == rhs.preloadIndex && lhs.hole == rhs.hole
    }
    
    static func <(lhs: HistoryPreloadHole, rhs: HistoryPreloadHole) -> Bool {
        return lhs.preloadIndex < rhs.preloadIndex
    }
    
    var hashValue: Int {
        return self.preloadIndex.index.hashValue &* 31 &+ self.hole.hashValue
    }
}

private final class HistoryPreloadEntry: Comparable {
    var hole: HistoryPreloadHole
    private var isStarted = false
    private let disposable = MetaDisposable()
    
    init(hole: HistoryPreloadHole) {
        self.hole = hole
    }
    
    static func ==(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
        return lhs.hole == rhs.hole
    }
    
    static func <(lhs: HistoryPreloadEntry, rhs: HistoryPreloadEntry) -> Bool {
        return lhs.hole < rhs.hole
    }
    
    func startIfNeeded(postbox: Postbox, accountPeerId: PeerId, download: Signal<Download, NoError>, queue: Queue) {
        if !self.isStarted {
            self.isStarted = true
            
            let hole = self.hole.hole
            let signal: Signal<Never, NoError> = .complete()
            |> delay(0.3, queue: queue)
            |> then(
                download
                |> take(1)
                |> deliverOn(queue)
                |> mapToSignal { download -> Signal<Never, NoError> in
                    switch hole.hole {
                        case let .peer(peerHole):
                            return fetchMessageHistoryHole(accountPeerId: accountPeerId, source: .download(download), postbox: postbox, peerId: peerHole.peerId, namespace: peerHole.namespace, direction: hole.direction, space: .everywhere, limit: 60)
                    }
                }
            )
            self.disposable.set(signal.start())
        }
    }
    
    deinit {
        self.disposable.dispose()
    }
}

private final class HistoryPreloadViewContext {
    var index: ChatListIndex?
    var hasUnread: Bool?
    var isMuted: Bool?
    var isPriority: Bool
    let disposable = MetaDisposable()
    var hole: MessageOfInterestHole?
    var media: [HolesViewMedia] = []
    
    var preloadIndex: HistoryPreloadIndex {
        return HistoryPreloadIndex(index: self.index, hasUnread: self.hasUnread ?? false, isMuted: self.isMuted ?? true, isPriority: self.isPriority)
    }
    
    var currentHole: HistoryPreloadHole? {
        if let hole = self.hole {
            return HistoryPreloadHole(preloadIndex: self.preloadIndex, hole: hole)
        } else {
            return nil
        }
    }
    
    init(index: ChatListIndex?, hasUnread: Bool?, isMuted: Bool?, isPriority: Bool) {
        self.index = index
        self.hasUnread = hasUnread
        self.isMuted = isMuted
        self.isPriority = isPriority
    }
    
    deinit {
        disposable.dispose()
    }
}

private enum ChatHistoryPreloadEntity: Hashable {
    case peer(PeerId)
}

private struct ChatHistoryPreloadIndex {
    let index: ChatListIndex
    let entity: ChatHistoryPreloadEntity
}

public final class ChatHistoryPreloadMediaItem: Comparable {
    public let preloadIndex: HistoryPreloadIndex
    public let media: HolesViewMedia
    
    init(preloadIndex: HistoryPreloadIndex, media: HolesViewMedia) {
        self.preloadIndex = preloadIndex
        self.media = media
    }
    
    public static func ==(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
        if lhs.preloadIndex != rhs.preloadIndex {
            return false
        }
        if lhs.media != rhs.media {
            return false
        }
        return true
    }
    
    public static func <(lhs: ChatHistoryPreloadMediaItem, rhs: ChatHistoryPreloadMediaItem) -> Bool {
        if lhs.preloadIndex != rhs.preloadIndex {
            return lhs.preloadIndex > rhs.preloadIndex
        }
        return lhs.media.index < rhs.media.index
    }
}

private final class AdditionalPreloadPeerIdsContext {
    private let queue: Queue
    
    private var subscribers: [PeerId: Bag<Void>] = [:]
    private var additionalPeerIdsValue = ValuePromise<Set<PeerId>>(Set(), ignoreRepeated: true)
    
    var additionalPeerIds: Signal<Set<PeerId>, NoError> {
        return self.additionalPeerIdsValue.get()
    }
    
    init(queue: Queue) {
        self.queue = queue
    }
    
    deinit {
        assert(self.queue.isCurrent())
    }
    
    func add(peerId: PeerId) -> Disposable {
        let bag: Bag<Void>
        if let current = self.subscribers[peerId] {
            bag = current
        } else {
            bag = Bag()
            self.subscribers[peerId] = bag
        }
        let wasEmpty = bag.isEmpty
        let index = bag.add(Void())
        
        if wasEmpty {
            self.additionalPeerIdsValue.set(Set(self.subscribers.keys))
        }
        let queue = self.queue
        return ActionDisposable { [weak self, weak bag] in
            queue.async {
                guard let strongSelf = self else {
                    return
                }
                if let current = strongSelf.subscribers[peerId], let bag = bag, current === bag {
                    current.remove(index)
                    if current.isEmpty {
                        strongSelf.subscribers.removeValue(forKey: peerId)
                        strongSelf.additionalPeerIdsValue.set(Set(strongSelf.subscribers.keys))
                    }
                }
            }
        }
    }
}

final class ChatHistoryPreloadManager {
    private let queue = Queue()
    
    private let postbox: Postbox
    private let accountPeerId: PeerId
    private let network: Network
    private let download = Promise<Download>()
    
    private var canPreloadHistoryDisposable: Disposable?
    private var canPreloadHistoryValue = false
    
    private let automaticChatListDisposable = MetaDisposable()
    
    private var views: [ChatHistoryPreloadEntity: HistoryPreloadViewContext] = [:]
    
    private var entries: [HistoryPreloadEntry] = []
    
    private var orderedMediaValue: [ChatHistoryPreloadMediaItem] = []
    private let orderedMediaPromise = ValuePromise<[ChatHistoryPreloadMediaItem]>([])
    var orderedMedia: Signal<[ChatHistoryPreloadMediaItem], NoError> {
        return self.orderedMediaPromise.get()
    }
    
    private let additionalPreloadPeerIdsContext:  QueueLocalObject<AdditionalPreloadPeerIdsContext>
    
    init(postbox: Postbox, network: Network, accountPeerId: PeerId, networkState: Signal<AccountNetworkState, NoError>) {
        self.postbox = postbox
        self.network = network
        self.accountPeerId = accountPeerId
        self.download.set(network.background())
        
        let queue = Queue.mainQueue()
        self.additionalPreloadPeerIdsContext = QueueLocalObject(queue: queue, generate: {
            AdditionalPreloadPeerIdsContext(queue: queue)
        })
        
        self.canPreloadHistoryDisposable = (networkState
        |> map { state -> Bool in
            switch state {
                case .online:
                    return true
                default:
                    return false
            }
        }
        |> distinctUntilChanged
        |> deliverOn(self.queue)).start(next: { [weak self] value in
            guard let strongSelf = self, strongSelf.canPreloadHistoryValue != value else {
                return
            }
            strongSelf.canPreloadHistoryValue = value
            if value {
                for i in 0 ..< min(3, strongSelf.entries.count) {
                    strongSelf.entries[i].startIfNeeded(postbox: strongSelf.postbox, accountPeerId: strongSelf.accountPeerId, download: strongSelf.download.get() |> take(1), queue: strongSelf.queue)
                }
            }
        })
    }
    
    deinit {
        self.canPreloadHistoryDisposable?.dispose()
    }
    
    func addAdditionalPeerId(peerId: PeerId) -> Disposable {
        let disposable = MetaDisposable()
        self.additionalPreloadPeerIdsContext.with { context in
            disposable.set(context.add(peerId: peerId))
        }
        return disposable
    }
    
    func start() {
        let additionalPreloadPeerIdsContext = self.additionalPreloadPeerIdsContext
        let additionalPeerIds = Signal<Set<PeerId>, NoError> { subscriber in
            let disposable = MetaDisposable()
            additionalPreloadPeerIdsContext.with { context in
                disposable.set(context.additionalPeerIds.start(next: { value in
                    subscriber.putNext(value)
                }))
            }
            return disposable
        }
        self.automaticChatListDisposable.set((combineLatest(queue: .mainQueue(), self.postbox.tailChatListView(groupId: .root, count: 20, summaryComponents: ChatListEntrySummaryComponents()), additionalPeerIds)
        |> delay(1.0, queue: .mainQueue())
        |> deliverOnMainQueue).start(next: { [weak self] view, additionalPeerIds in
            guard let strongSelf = self else {
                return
            }
            #if DEBUG
            if true {
                //return
            }
            #endif
            var indices: [(ChatHistoryPreloadIndex, Bool, Bool)] = []
            for entry in view.0.entries {
                if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _) = entry {
                    var hasUnread = false
                    if let readState = readState {
                        hasUnread = readState.count != 0
                    }
                    var isMuted = false
                    if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
                        if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
                            isMuted = true
                        }
                    }
                    indices.append((ChatHistoryPreloadIndex(index: index, entity: .peer(index.messageIndex.id.peerId)), hasUnread, isMuted))
                }
            }
            
            strongSelf.update(indices: indices, additionalPeerIds: additionalPeerIds)
        }))
    }
    
    private func update(indices: [(ChatHistoryPreloadIndex, Bool, Bool)], additionalPeerIds: Set<PeerId>) {
        self.queue.async {
            var validEntityIds = Set(indices.map { $0.0.entity })
            for peerId in additionalPeerIds {
                validEntityIds.insert(.peer(peerId))
            }
            
            var removedEntityIds: [ChatHistoryPreloadEntity] = []
            for (entityId, view) in self.views {
                if !validEntityIds.contains(entityId) {
                    removedEntityIds.append(entityId)
                    if let hole = view.currentHole {
                        self.update(from: hole, to: nil)
                    }
                }
            }
            for entityId in removedEntityIds {
                self.views.removeValue(forKey: entityId)
            }
            
            var combinedIndices: [(ChatHistoryPreloadIndex, Bool, Bool, Bool)] = []
            var existingPeerIds = Set<PeerId>()
            for (index, hasUnread, isMuted) in indices {
                existingPeerIds.insert(index.index.messageIndex.id.peerId)
                combinedIndices.append((index, hasUnread, isMuted, additionalPeerIds.contains(index.index.messageIndex.id.peerId)))
            }
            for peerId in additionalPeerIds {
                if !existingPeerIds.contains(peerId) {
                    combinedIndices.append((ChatHistoryPreloadIndex(index: ChatListIndex.absoluteLowerBound, entity: .peer(peerId)), false, true, true))
                }
            }
            
            for (index, hasUnread, isMuted, isPriority) in combinedIndices {
                if let view = self.views[index.entity] {
                    if view.index != index.index || view.hasUnread != hasUnread || view.isMuted != isMuted {
                        let previousHole = view.currentHole
                        view.index = index.index
                        view.hasUnread = hasUnread
                        view.isMuted = isMuted
                        
                        let updatedHole = view.currentHole
                        if previousHole != updatedHole {
                            self.update(from: previousHole, to: updatedHole)
                        }
                    }
                } else {
                    let view = HistoryPreloadViewContext(index: index.index, hasUnread: hasUnread, isMuted: isMuted, isPriority: isPriority)
                    self.views[index.entity] = view
                    let key: PostboxViewKey
                    switch index.entity {
                        case let .peer(peerId):
                            key = .messageOfInterestHole(location: .peer(peerId), namespace: Namespaces.Message.Cloud, count: 60)
                    }
                    view.disposable.set((self.postbox.combinedView(keys: [key])
                    |> deliverOn(self.queue)).start(next: { [weak self] next in
                        if let strongSelf = self, let value = next.views[key] as? MessageOfInterestHolesView {
                            if let view = strongSelf.views[index.entity] {
                                let previousHole = view.currentHole
                                view.hole = value.closestHole
                                
                                var mediaUpdated = false
                                if view.media.count != value.closestLaterMedia.count {
                                    mediaUpdated = true
                                } else {
                                    for i in 0 ..< view.media.count {
                                        if view.media[i] != value.closestLaterMedia[i] {
                                            mediaUpdated = true
                                            break
                                        }
                                    }
                                }
                                if mediaUpdated {
                                    view.media = value.closestLaterMedia
                                    strongSelf.updateMedia()
                                }
                                
                                let updatedHole = view.currentHole
                                if previousHole != updatedHole {
                                    strongSelf.update(from: previousHole, to: updatedHole)
                                }
                            }
                        }
                    }))
                }
            }
        }
    }
    
    private func updateMedia() {
        var result: [ChatHistoryPreloadMediaItem] = []
        for (_, view) in self.views {
            for media in view.media {
                result.append(ChatHistoryPreloadMediaItem(preloadIndex: view.preloadIndex, media: media))
            }
        }
        result.sort()
        if result != self.orderedMediaValue {
            self.orderedMediaValue = result
            self.orderedMediaPromise.set(result)
        }
    }
    
    private func update(from previousHole: HistoryPreloadHole?, to updatedHole: HistoryPreloadHole?) {
        assert(self.queue.isCurrent())
        if previousHole == updatedHole {
            return
        }
        
        var skipUpdated = false
        if let previousHole = previousHole {
            for i in (0 ..< self.entries.count).reversed() {
                if self.entries[i].hole == previousHole {
                    if let updatedHole = updatedHole, updatedHole.hole == self.entries[i].hole.hole {
                        self.entries[i].hole = updatedHole
                        skipUpdated = true
                    } else {
                        self.entries.remove(at: i)
                    }
                    break
                }
            }
        }
        
        if let updatedHole = updatedHole, !skipUpdated {
            var found = false
            for i in 0 ..< self.entries.count {
                if self.entries[i].hole == updatedHole {
                    found = true
                    break
                }
            }
            if !found {
                self.entries.append(HistoryPreloadEntry(hole: updatedHole))
                self.entries.sort()
            }
        }
        
        if self.canPreloadHistoryValue {
            for i in 0 ..< min(3, self.entries.count) {
                self.entries[i].startIfNeeded(postbox: self.postbox, accountPeerId: self.accountPeerId, download: self.download.get() |> take(1), queue: self.queue)
            }
        }
    }
}