//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

//  Originally based on EPContacts
//
//  Created by Prabaharan Elangovan on 12/10/15.
//  Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved

import UIKit
import Contacts
import SessionServiceKit

@objc
public protocol ContactsPickerDelegate: class {
    func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError)
    func contactsPickerDidCancel(_: ContactsPicker)
    func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
    func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
    func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
}

@objc
public enum SubtitleCellValue: Int {
    case phoneNumber, email, none
}

@objc
public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {

    var tableView: UITableView!
    var searchBar: UISearchBar!

    // MARK: - Properties

    private let contactCellReuseIdentifier = "contactCellReuseIdentifier"

    private var contactsManager: OWSContactsManager {
        return Environment.shared.contactsManager
    }

    // HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
    // If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
    // the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
    override public var canBecomeFirstResponder: Bool {
        Logger.debug("")
        return true
    }

    override public func becomeFirstResponder() -> Bool {
        Logger.debug("")
        return super.becomeFirstResponder()
    }

    override public func resignFirstResponder() -> Bool {
        Logger.debug("")
        return super.resignFirstResponder()
    }

    private let collation = UILocalizedIndexedCollation.current()
    public var collationForTests: UILocalizedIndexedCollation {
        get {
            return collation
        }
    }
    private let contactStore = CNContactStore()

    // Data Source State
    private lazy var sections = [[CNContact]]()
    private lazy var filteredSections = [[CNContact]]()
    private lazy var selectedContacts = [Contact]()

    // Configuration
    @objc
    public weak var contactsPickerDelegate: ContactsPickerDelegate?
    private let subtitleCellType: SubtitleCellValue
    private let allowsMultipleSelection: Bool
    private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys

    // MARK: - Initializers

    @objc
    required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
        self.allowsMultipleSelection = allowsMultipleSelection
        self.subtitleCellType = subtitleCellType
        super.init(nibName: nil, bundle: nil)
    }

    required public init?(coder aDecoder: NSCoder) {
        notImplemented()
    }

    // MARK: - Lifecycle Methods

    override public func loadView() {
        self.view = UIView()
        let tableView = UITableView()
        self.tableView = tableView
        self.tableView.separatorColor = Theme.cellSeparatorColor

        view.addSubview(tableView)
        tableView.autoPinEdge(toSuperviewEdge: .top)
        tableView.autoPinEdge(toSuperviewEdge: .bottom)
        tableView.autoPinEdge(toSuperviewSafeArea: .leading)
        tableView.autoPinEdge(toSuperviewSafeArea: .trailing)
        tableView.delegate = self
        tableView.dataSource = self

        let searchBar = OWSSearchBar()
        self.searchBar = searchBar
        searchBar.delegate = self
        searchBar.sizeToFit()

        tableView.tableHeaderView = searchBar
    }

    override open func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = Theme.backgroundColor
        self.tableView.backgroundColor = Theme.backgroundColor

        searchBar.placeholder = NSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")

        // Auto size cells for dynamic type
        tableView.estimatedRowHeight = 60.0
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 60

        tableView.allowsMultipleSelection = allowsMultipleSelection

        tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)

        registerContactCell()
        initializeBarButtons()
        reloadContacts()
        updateSearchResults(searchText: "")

        NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
    }

    @objc
    public func didChangePreferredContentSize() {
        self.tableView.reloadData()
    }

    private func initializeBarButtons() {
        let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
        self.navigationItem.leftBarButtonItem = cancelButton

        if allowsMultipleSelection {
            let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
            self.navigationItem.rightBarButtonItem = doneButton
        }
    }

    private func registerContactCell() {
        tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
    }

    // MARK: - Contact Operations

    private func reloadContacts() {
        getContacts( onError: { error in
            Logger.error("failed to reload contacts with error:\(error)")
        })
    }

    private func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
        switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
            case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
                let title = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE", comment: "Alert title when contacts disabled while trying to invite contacts to signal")
                let body = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY", comment: "Alert body when contacts disabled while trying to invite contacts to signal")

                let alert = UIAlertController(title: title, message: body, preferredStyle: .alert)

                let dismissText = CommonStrings.cancelButton

                let cancelAction = UIAlertAction(title: dismissText, style: .cancel, handler: {  _ in
                    let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
                    self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
                    errorHandler(error)
                })
                alert.addAction(cancelAction)

                let settingsText = CommonStrings.openSettingsButton
                let openSettingsAction = UIAlertAction(title: settingsText, style: .default, handler: { (_) in
                    UIApplication.shared.openSystemSettings()
                })
                alert.addAction(openSettingsAction)

                self.presentAlert(alert)

            case CNAuthorizationStatus.notDetermined:
                //This case means the user is prompted for the first time for allowing contacts
                contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
                    //At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
                    if granted {
                        self.getContacts(onError: errorHandler)
                    } else {
                       errorHandler(error!)
                    }
                }

            case  CNAuthorizationStatus.authorized:
                //Authorization granted by user for this app.
                var contacts = [CNContact]()

                do {
                    let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
                    contactFetchRequest.sortOrder = .userDefault
                    try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
                        contacts.append(contact)
                    }
                    self.sections = collatedContacts(contacts)
                } catch let error as NSError {
                    Logger.error("Failed to fetch contacts with error:\(error)")
                }
        }
    }

    func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
        let selector: Selector = #selector(getter: CNContact.nameForCollating)

        var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
        for contact in contacts {
            let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
            collated[sectionNumber].append(contact)
        }
        return collated
    }

    // MARK: - Table View DataSource

    open func numberOfSections(in tableView: UITableView) -> Int {
        return self.collation.sectionTitles.count
    }

    open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let dataSource = filteredSections

        guard section < dataSource.count else {
            return 0
        }

        return dataSource[section].count
    }

    // MARK: - Table View Delegates

    open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
            owsFailDebug("cell had unexpected type")
            return UITableViewCell()
        }

        let dataSource = filteredSections
        let cnContact = dataSource[indexPath.section][indexPath.row]
        let contact = Contact(systemContact: cnContact)

        cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
        let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
        cell.isSelected = isSelected

        // Make sure we preserve selection across tableView.reloadData which happens when toggling between 
        // search controller
        if (isSelected) {
            self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
        } else {
            self.tableView.deselectRow(at: indexPath, animated: false)
        }

        return cell
    }

    open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath) as! ContactCell
        let deselectedContact = cell.contact!

        selectedContacts = selectedContacts.filter {
            return $0.uniqueId != deselectedContact.uniqueId
        }
    }

    open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        Logger.verbose("")

        let cell = tableView.cellForRow(at: indexPath) as! ContactCell
        let selectedContact = cell.contact!

        guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
            self.tableView.deselectRow(at: indexPath, animated: false)
            return
        }

        selectedContacts.append(selectedContact)

        if !allowsMultipleSelection {
            // Single selection code
            self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
        }
    }

    open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
        return collation.section(forSectionIndexTitle: index)
    }

    open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        return collation.sectionIndexTitles
    }

    open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        let dataSource = filteredSections

        guard section < dataSource.count else {
            return nil
        }

        // Don't show empty sections
        if dataSource[section].count > 0 {
            guard section < collation.sectionTitles.count else {
                return nil
            }

            return collation.sectionTitles[section]
        } else {
            return nil
        }
    }

    // MARK: - Button Actions

    @objc func onTouchCancelButton() {
        contactsPickerDelegate?.contactsPickerDidCancel(self)
    }

    @objc func onTouchDoneButton() {
        contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
    }

    // MARK: - Search Actions
    open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        updateSearchResults(searchText: searchText)
    }

    open func updateSearchResults(searchText: String) {
        let predicate: NSPredicate
        if searchText.isEmpty {
            filteredSections = sections
        } else {
            do {
                predicate = CNContact.predicateForContacts(matchingName: searchText)
                let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
                    filteredSections = collatedContacts(filteredContacts)
            } catch let error as NSError {
                Logger.error("updating search results failed with error: \(error)")
            }
        }
        self.tableView.reloadData()
    }
}

let ContactSortOrder = computeSortOrder()

func computeSortOrder() -> CNContactSortOrder {
    let comparator = CNContact.comparator(forNameSortOrder: .userDefault)

    let contact0 = CNMutableContact()
    contact0.givenName = "A"
    contact0.familyName = "Z"

    let contact1 = CNMutableContact()
    contact1.givenName = "Z"
    contact1.familyName = "A"

    let result = comparator(contact0, contact1)

    if result == .orderedAscending {
        return .givenName
    } else {
        return .familyName
    }
}

fileprivate extension CNContact {
    /**
     * Sorting Key used by collation
     */
    @objc var nameForCollating: String {
        get {
            if self.familyName.isEmpty && self.givenName.isEmpty {
                return self.emailAddresses.first?.value as String? ?? ""
            }

            let compositeName: String
            if ContactSortOrder == .familyName {
                compositeName = "\(self.familyName) \(self.givenName)"
            } else {
                compositeName = "\(self.givenName) \(self.familyName)"
            }
            return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        }
    }
}
