//
//  SearchViewController.swift
//  CutBox
//
//  Created by Jason Milkins on 3/4/18.
//  Copyright © 2018-2023 ocodo. All rights reserved.
//

import Cocoa
import RxSwift

/// Controller for `SearchAndPreviewView`
///
/// Subscribes any `SearchViewEvents` (from `SearchAndPreviewView`),
/// performing the required actions,  using init injected: `searchView:SearchAndPreviewView`,
/// `historyService:HistoryService` and `prefs:CutBoxPreferencesService`.
class SearchViewController: NSObject {
    var searchView: SearchAndPreviewView
    var historyService: HistoryService
    var prefs: CutBoxPreferencesService
    var fakeKey: FakeKey

    var orderedSelection: OrderedSet<Int> = OrderedSet<Int>()

    var searchPopup: PopupController

    /// Event stream from SearchAndPreviewView
    var events: PublishSubject<SearchViewEvents> {
        return self.searchView.events
    }

    /// Row indexes of selected items
    var selectedItems: IndexSet? {
        return self.searchView.selectedItems
    }

    /// Strings of selected items
    var selectedClips: [String] {
        if self.historyService.items.isEmpty {
            return []
        }

        return self.orderedSelection.all()
            .map { self.historyService.items[$0] }
    }

    let disposeBag = DisposeBag()

    /// Setup controller, initialize the search view and popup.
    /// Connect the history service and tell it to start polling the pasteboard.
    /// Connect the preferences service to self and members.
    init(historyService: HistoryService = HistoryService.shared,
         cutBoxPreferences: CutBoxPreferencesService = CutBoxPreferencesService.shared,
         fakeKey: FakeKey = FakeKey(),
         searchView: SearchAndPreviewView = SearchAndPreviewView.fromNib()!
    ) {
        self.historyService = historyService
        self.prefs = cutBoxPreferences
        self.fakeKey = fakeKey

        self.searchView = searchView
        self.searchPopup = PopupController(content: self.searchView)

        self.historyService.beginPolling()

        super.init()

        configureSearchPopupAndView()
    }

    /// Setup the context menu for items shown in the search view
    func setupClipItemsContextMenu() {
        self.searchView.setupClipItemsContextMenu()
        self.searchView.menuDelegate = self
    }

    /// Toggle display of the search popup
    func togglePopup() {
        self.historyService.favoritesOnly = false
        self.searchView.applyTheme()
        self.searchPopup.togglePopup()
    }

    /// Close the search popup
    func closeSearchPopup() {
        self.historyService.favoritesOnly = false
        self.searchPopup.closePopup()
    }

    /// Open the search popup
    func openSearchPopup() {
        self.historyService.favoritesOnly = false
        self.searchPopup.openPopup()
    }

    /// Hide the app (objc selector)
    @objc func hideApp() {
        NSApp.hide(self)
    }

    /// send a fake paste (Cmd V) to macOS
    @objc func fakePaste() {
        fakeKey.send(fakeKey: "V", useCommandFlag: true)
    }

    private func justClose() {
        self.closeSearchPopup()
        perform(#selector(hideApp), with: self, afterDelay: 0.1)
    }

    private func closeAndPaste(useJS: Bool = false) {
        self.pasteSelectedClipToPasteboard(useJS)
        self.closeSearchPopup()
        perform(#selector(hideApp), with: self, afterDelay: 0.1)
        perform(#selector(fakePaste), with: self, afterDelay: 0.25)
    }

    /// Remove selected items and refresh the search view
    func removeSelectedItems() {
        if let selection = self.selectedItems {
            self.historyService.remove(selected: selection)
            self.searchView.reloadData()
        }
    }

    /// Toggle favorite status on selected items
    func toggleFavoriteItems() {
        if  let selection = self.selectedItems {
            self.historyService.toggleFavorite(items: selection)
            self.searchView.reloadData()
            self.searchView.selectRowIndexes(selection, byExtendingSelection: false)
        }
    }

    /// Send the selected clip text to the pasteboard
    func pasteSelectedClipToPasteboard(_ useJS: Bool) {
        guard !self.selectedClips.isEmpty else {
            return
        }

        pasteToPasteboard(self.selectedClips)
    }

    private func pasteToPasteboard(_ clips: [String]) {
        let clip = prefs.prepareClips(clips)

        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(clip, forType: .string)
    }

    private func pasteToPasteboard(_ clip: String) {
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(clip, forType: .string)
    }

    private func resetSearchText() {
        self.searchView.searchText?.string = ""
        self.searchView.filterTextPublisher.onNext("")
        self.searchView.reloadData()
    }

    private func setupSearchViewAndFilterBinding() {
        self.searchView.itemsDataSource = self
        self.searchView.itemsDelegate = self

        self.searchView.filterTextPublisher
            .map { $0.isEmpty }
            .subscribe(onNext: {
                if self.prefs.useCompactUI {
                    self.searchView.hideSearchResults($0)
                } else {
                    self.searchView.hideSearchResults(false)
                }
            })
            .disposed(by: disposeBag)

        self.searchView.filterTextPublisher
            .subscribe(onNext: {
                self.historyService.filterText = $0
                self.searchView.reloadData()
                self.searchView.scrollRowToVisible(0)
            })
            .disposed(by: self.disposeBag)

        self.prefs.events
            .compactMap {
                if case .hidePreviewSettingChanged(let isOn) = $0 {
                    return isOn
                }
                return nil
            }
            .subscribe(onNext: {
                self.searchView.hidePreview($0)
            })
            .disposed(by: self.disposeBag)
    }

    /// Update the preview with selected clip(s)
    func updateSearchItemPreview() {
        let preview = prefs.prepareClips(selectedClips)
        self.searchView.previewString = preview.truncate(limit: 50_000)
    }

    // swiftlint:disable cyclomatic_complexity
    // swiftlint:disable function_body_length
    private func setupSearchTextEventBindings() {
        self.events
            .subscribe(onNext: onNext)
            .disposed(by: self.disposeBag)
    }

    func onNext(event: SearchViewEvents) {
        switch event {
        case .setSearchMode(let mode):
            self.historyService.searchMode = mode
            self.searchView.reloadData()
            self.searchView.setSearchModeButton(mode: mode)

        case .toggleSearchMode:
            let mode = self.historyService.toggleSearchMode()
            self.searchView.reloadData()
            self.searchView.setSearchModeButton(mode: mode)

        case .setTimeFilter(let seconds):
            self.historyService.setTimeFilter(seconds: seconds)
            self.searchView.reloadData()

        case .toggleTimeFilter:
            self.searchView.toggleTimeFilter()
            self.historyService.setTimeFilter(seconds: nil)
            self.searchView.reloadData()

        case .cycleTheme:
            self.prefs.cycleTheme()
            self.searchView.applyTheme()
            self.reloadDataWithExistingSelection()

        case .toggleWrappingStrings:
            self.prefs.useWrappingStrings.toggle()
            self.updateSearchItemPreview()

        case .toggleJoinStrings:
            self.prefs.useJoinString.toggle()
            self.updateSearchItemPreview()

        case .toggleSearchScope:
            self.historyService.favoritesOnly.toggle()
            self.searchView.reloadData()
            self.searchView.setSearchScopeButton(favoritesOnly: self.historyService.favoritesOnly)

        case .togglePreview:
            self.prefs.hidePreview.toggle()

        case .scaleTextDown:
            self.prefs.scaleTextDown()
            self.reloadDataWithExistingSelection()

        case .scaleTextUp:
            self.prefs.scaleTextUp()
            self.reloadDataWithExistingSelection()

        case .scaleTextNormalize:
            self.prefs.scaleTextNormalize()
            self.reloadDataWithExistingSelection()

        case .toggleFavorite:
            self.toggleFavoriteItems()

        case .justClose:
            self.justClose()

        case .closeAndPasteSelected:
            self.closeAndPaste()

        case .removeSelected:
            self.removeSelectedItems()

        default:
            break
        }
    }
    // swiftlint:enable cyclomatic_complexity
    // swiftlint:enable function_body_length

    private func reloadDataWithExistingSelection() {
        if let selected = self.searchView.selectedRowIndexes {
            self.searchView.updateLayer()
            self.searchView.reloadData()
            self.searchView.setTextScale()
            self.searchView.selectRowIndexes(selected, byExtendingSelection: false)
        }
    }

    private func configureSearchPopupAndView() {
        setupSearchTextEventBindings()
        setupSearchViewAndFilterBinding()
        setupClipItemsContextMenu()

        self.searchView.placeHolderTextString = "search_placeholder".l7n

        self.searchPopup.proportionalTopPadding = 0.15
        self.searchPopup.proportionalWidth = 0.6
        self.searchPopup.proportionalHeight = 0.6

        self.searchPopup.willOpenPopup = self.searchPopup.proportionalResizePopup

        self.searchPopup.didOpenPopup = {
            guard let window = self.searchView.window
                else { fatalError("No window found for popup") }

            self.resetSearchText()

            // Focus search text
            window.makeFirstResponder(self.searchView.searchText)
        }

        self.searchPopup.willClosePopup = self.resetSearchText
    }
}
