// Copyright © 2015 Abhishek Banthia

import Cocoa
import CoreLoggerKit
import CoreModelKit

private enum MenubarState {
    case compactText
    case standardText
    case icon
}

class StatusItemHandler: NSObject {
    var hasActiveIcon: Bool = false

    var menubarTimer: Timer?

    var statusItem: NSStatusItem = {
        let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        statusItem.toolTip = "Clocker"
        statusItem.highlightMode = false
        return statusItem
    }()

    private lazy var menubarTitleHandler = MenubarTitleProvider(with: self.store, eventStore: EventCenter.sharedCenter())

    private var statusContainerView: StatusContainerView?

    private var nsCalendar = Calendar.autoupdatingCurrent

    private lazy var units: Set<Calendar.Component> = Set([.era, .year, .month, .day, .hour, .minute])

    private var userNotificationsDidChangeNotif: NSObjectProtocol?

    private let store: DataStore

    // Current State might be set twice when the user first launches an app.
    // First, when StatusItemHandler() is instantiated in AppDelegate
    // Second, when AppDelegate.fetchLocalTimezone() is called triggering a customLabel didSet.
    // TODO: Make sure it's set just once.
    private var currentState: MenubarState = .standardText {
        didSet {
            // Do some cleanup
            switch oldValue {
            case .compactText:
                statusItem.view = nil
                statusContainerView = nil
            case .standardText:
                statusItem.button?.title = CLEmptyString
            case .icon:
                statusItem.button?.image = nil
            }

            // Now setup for the new menubar state
            switch currentState {
            case .compactText:
                setupForCompactTextMode()
            case .standardText:
                setupForStandardTextMode()
            case .icon:
                setClockerIcon()
            }

            Logger.info("Status Bar Current State changed: \(currentState)\n")
        }
    }

    init(with dataStore: DataStore) {
        store = dataStore
        super.init()

        setupStatusItem()
        setupNotificationObservers()
    }

    func setupStatusItem() {
        // Let's figure out the initial menubar state
        var menubarState = MenubarState.icon

        let shouldTextBeDisplayed = store.menubarTimezones()?.isEmpty ?? true

        if !shouldTextBeDisplayed || store.shouldDisplay(.showMeetingInMenubar) {
            if store.shouldDisplay(.menubarCompactMode) {
                menubarState = .compactText
            } else {
                menubarState = .standardText
            }
        }

        // Initial state has been figured out. Time to set it!
        currentState = menubarState

        func setSelector() {
            if #available(macOS 10.14, *) {
                statusItem.button?.action = #selector(menubarIconClicked(_:))
            } else {
                statusItem.action = #selector(menubarIconClicked(_:))
            }
        }

        statusItem.target = self
        statusItem.autosaveName = NSStatusItem.AutosaveName("ClockerStatusItem")
        setSelector()
    }

    private func setupNotificationObservers() {
        let center = NotificationCenter.default
        let mainQueue = OperationQueue.main

        center.addObserver(self,
                           selector: #selector(updateMenubar),
                           name: NSWorkspace.didWakeNotification,
                           object: nil)

        DistributedNotificationCenter.default.addObserver(self, selector: #selector(respondToInterfaceStyleChange),
                                                          name: .interfaceStyleDidChange,
                                                          object: nil)

        userNotificationsDidChangeNotif = center.addObserver(forName: UserDefaults.didChangeNotification,
                                                             object: self,
                                                             queue: mainQueue)
        { _ in
            self.setupStatusItem()
        }

        NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: OperationQueue.main) { _ in
            self.menubarTimer?.invalidate()
        }

        NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: OperationQueue.main) { _ in
            self.setupStatusItem()
        }
    }

    deinit {
        if let userNotifsDidChange = userNotificationsDidChangeNotif {
            NotificationCenter.default.removeObserver(userNotifsDidChange)
        }
    }

    private func constructCompactView(with upcomingEventView: Bool = false) {
        statusContainerView = nil

        let menubarTimezones = store.menubarTimezones() ?? []
        if menubarTimezones.isEmpty {
            currentState = .icon
            return
        }

        statusContainerView = StatusContainerView(with: menubarTimezones,
                                                  store: store,
                                                  showUpcomingEventView: upcomingEventView,
                                                  bufferContainerWidth: bufferCalculatedWidth())
        statusItem.view = statusContainerView
        statusItem.view?.window?.backgroundColor = NSColor.clear
    }

    // This is called when the Apple interface style pre-Mojave is changed.
    // In High Sierra and before, we could have a dark or light menubar and dock
    // Our icon is template, so it changes automatically; so is our standard status bar text
    // Only need to handle the compact mode!
    @objc func respondToInterfaceStyleChange() {
        if store.shouldDisplay(.menubarCompactMode) {
            updateCompactMenubar()
        }
    }

    @objc func setHasActiveIcon(_ value: Bool) {
        hasActiveIcon = value
    }

    @objc func menubarIconClicked(_ sender: Any) {
        guard let mainDelegate = NSApplication.shared.delegate as? AppDelegate else {
            return
        }

        mainDelegate.togglePanel(sender)
    }

    @objc func updateMenubar() {
        guard let fireDate = calculateFireDate() else { return }

        let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()

        menubarTimer = Timer(fire: fireDate,
                             interval: 0,
                             repeats: false,
                             block: { [weak self] _ in

                                 if let strongSelf = self {
                                     strongSelf.refresh()
                                 }
                             })

        // Tolerance, even a small amount, has a positive imapct on the power usage. As a rule, we set it to 10% of the interval
        menubarTimer?.tolerance = shouldDisplaySeconds ? 0.5 : 20

        guard let runLoopTimer = menubarTimer else {
            Logger.info("Timer is unexpectedly nil")
            return
        }

        RunLoop.main.add(runLoopTimer, forMode: .common)
    }

    private func shouldDisplaySecondsInMenubar() -> Bool {
        let syncedTimezones = store.menubarTimezones() ?? []

        let timezonesSupportingSeconds = syncedTimezones.filter { data in
            if let timezoneObj = TimezoneData.customObject(from: data) {
                return timezoneObj.shouldShowSeconds(store.timezoneFormat())
            }
            return false
        }

        return timezonesSupportingSeconds.isEmpty == false
    }

    private func calculateFireDate() -> Date? {
        let shouldDisplaySeconds = shouldDisplaySecondsInMenubar()
        let menubarFavourites = store.menubarTimezones()

        if !units.contains(.second), shouldDisplaySeconds {
            units.insert(.second)
        }

        var components = nsCalendar.dateComponents(units, from: Date())

        // We want to update every second only when there's a timezone present!
        if shouldDisplaySeconds, let seconds = components.second, let favourites = menubarFavourites, !favourites.isEmpty {
            components.second = seconds + 1
        } else if let minutes = components.minute {
            components.minute = minutes + 1
        } else {
            Logger.info("Unable to create date components for the menubar timewr")
            return nil
        }

        guard let fireDate = nsCalendar.date(from: components) else {
            Logger.info("Unable to form Fire Date")
            return nil
        }

        return fireDate
    }

    func updateCompactMenubar() {
        let filteredEvents = EventCenter.sharedCenter().filteredEvents
        let calendar = EventCenter.sharedCenter().autoupdatingCalendar
        let upcomingEvent = menubarTitleHandler.checkForUpcomingEvents(filteredEvents, calendar: calendar)
        if upcomingEvent != nil {
            // Iterate and see if we're showing the calendar item view
            let upcomingEventView = retrieveUpcomingEventStatusView()
            // If not, reconstruct Status Container View with another view
            if upcomingEventView == nil {
                constructCompactView(with: true)
            }
        }

        if let upcomingEventView = retrieveUpcomingEventStatusView(), upcomingEvent == nil {
            upcomingEventView.removeFromSuperview()
            constructCompactView() // So that Status Container View reclaims the space
        }
        // This will internally call `statusItemViewSetNeedsDisplay` on all subviews ensuring all text in the menubar is up-to-date.
        statusContainerView?.updateTime()
    }

    private func removeUpcomingStatusItemView() {
        NSAnimationContext.runAnimationGroup({ context in
            context.duration = 0.2
            let upcomingEventView = retrieveUpcomingEventStatusView()
            upcomingEventView?.removeFromSuperview()
        }) { [weak self] in
            if let sSelf = self {
                sSelf.constructCompactView()
            }
        }
    }

    func refresh() {
        if currentState == .compactText {
            updateCompactMenubar()
            updateMenubar()
        } else if currentState == .standardText, let title = menubarTitleHandler.titleForMenubar() {
            // Need setting button's image to nil
            // Especially if we have showUpcomingEvents turned to true and menubar timezones are empty
            statusItem.button?.image = nil
            statusItem.button?.title = title
            updateMenubar()
        } else {
            setClockerIcon()
            menubarTimer?.invalidate()
        }
    }

    private func setupForStandardTextMode() {
        Logger.info("Initializing menubar timer")

        // Let's invalidate the previous timer
        menubarTimer?.invalidate()
        menubarTimer = nil

        setupForStandardText()
        updateMenubar()
    }

    func invalidateTimer(showIcon show: Bool, isSyncing sync: Bool) {
        // Check if user is not showing
        // 1. Timezones
        // 2. Upcoming Event
        let menubarFavourites = store.menubarTimezones() ?? []

        if menubarFavourites.isEmpty, store.shouldDisplay(.showMeetingInMenubar) == false {
            Logger.info("Invalidating menubar timer!")

            invalidation()

            if show {
                currentState = .icon
            }

        } else if sync {
            Logger.info("Invalidating menubar timer for sync purposes!")

            invalidation()

            if show {
                setClockerIcon()
            }

        } else {
            Logger.info("Not stopping menubar timer!")
        }
    }

    private func invalidation() {
        menubarTimer?.invalidate()
    }

    private func setClockerIcon() {
        if statusItem.view != nil {
            statusItem.view = nil
        }

        if statusItem.button?.image?.name() == NSImage.Name.menubarIcon {
            return
        }

        statusItem.button?.title = CLEmptyString
        statusItem.button?.image = NSImage(named: .menubarIcon)
        statusItem.button?.imagePosition = .imageOnly
        statusItem.toolTip = "Clocker"
    }

    private func setupForStandardText() {
        var menubarText = CLEmptyString

        if let menubarTitle = menubarTitleHandler.titleForMenubar() {
            menubarText = menubarTitle
        } else if store.shouldDisplay(.showMeetingInMenubar) {
            // Don't have any meeting to show
        } else {
            // We have no favourites to display and no meetings to show.
            // That means we should display our icon!
        }

        guard !menubarText.isEmpty else {
            setClockerIcon()
            return
        }

        let attributes = [NSAttributedString.Key.font: NSFont.monospacedDigitSystemFont(ofSize: 13.0, weight: NSFont.Weight.regular),
                          NSAttributedString.Key.baselineOffset: 0.1] as [NSAttributedString.Key: Any]
        statusItem.button?.attributedTitle = NSAttributedString(string: menubarText, attributes: attributes)
        statusItem.button?.image = nil
        statusItem.button?.imagePosition = .imageLeft
    }

    private func setupForCompactTextMode() {
        // Let's invalidate the previous timer
        menubarTimer?.invalidate()
        menubarTimer = nil

        let filteredEvents = EventCenter.sharedCenter().filteredEvents
        let calendar = EventCenter.sharedCenter().autoupdatingCalendar
        let checkForUpcomingEvents = menubarTitleHandler.checkForUpcomingEvents(filteredEvents, calendar: calendar)
        constructCompactView(with: checkForUpcomingEvents != nil)
        updateMenubar()
    }

    private func retrieveUpcomingEventStatusView() -> NSView? {
        let upcomingEventView = statusContainerView?.subviews.first(where: { statusItemView in
            if let upcomingEventView = statusItemView as? StatusItemViewConforming {
                return upcomingEventView.statusItemViewIdentifier() == "upcoming_event_view"
            }
            return false
        })
        return upcomingEventView
    }

    private func bufferCalculatedWidth() -> Int {
        var totalWidth = 55

        if store.shouldShowDayInMenubar() {
            totalWidth += 12
        }

        if store.isBufferRequiredForTwelveHourFormats() {
            totalWidth += 20
        }

        if store.shouldShowDateInMenubar() {
            totalWidth += 20
        }

        if store.shouldDisplay(.showMeetingInMenubar) {
            totalWidth += 100
        }

        return totalWidth
    }
}