import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import SyncCore
import MessageUI
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import TelegramStringFormatting
import AccountContext
import AlertUI
import PresentationDataUtils
import PhotoResources
import MediaResources
import LocationResources
import ItemListAvatarAndNameInfoItem
import Geocoding
import ItemListAddressItem
import LocalizedPeerData
import PhoneNumberFormat

private enum DeviceContactInfoAction {
    case sendMessage
    case createContact
    case addToExisting
    case invite
}

private final class DeviceContactInfoControllerArguments {
    let context: AccountContext
    let isPlain: Bool
    let updateEditingName: (ItemListAvatarAndNameInfoItemName) -> Void
    let updatePhone: (Int64, String) -> Void
    let updatePhoneLabel: (Int64, String) -> Void
    let deletePhone: (Int64) -> Void
    let setPhoneIdWithRevealedOptions: (Int64?, Int64?) -> Void
    let addPhoneNumber: () -> Void
    let performAction: (DeviceContactInfoAction) -> Void
    let toggleSelection: (DeviceContactInfoDataId) -> Void
    let callPhone: (String) -> Void
    let openUrl: (String) -> Void
    let openAddress: (DeviceContactAddressData) -> Void
    let displayCopyContextMenu: (DeviceContactInfoEntryTag, String) -> Void
    let updateShareViaException: (Bool) -> Void
    
    init(context: AccountContext, isPlain: Bool, updateEditingName: @escaping (ItemListAvatarAndNameInfoItemName) -> Void, updatePhone: @escaping (Int64, String) -> Void, updatePhoneLabel: @escaping (Int64, String) -> Void, deletePhone: @escaping (Int64) -> Void, setPhoneIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, addPhoneNumber: @escaping () -> Void, performAction: @escaping (DeviceContactInfoAction) -> Void, toggleSelection: @escaping (DeviceContactInfoDataId) -> Void, callPhone: @escaping (String) -> Void, openUrl: @escaping (String) -> Void, openAddress: @escaping (DeviceContactAddressData) -> Void, displayCopyContextMenu: @escaping (DeviceContactInfoEntryTag, String) -> Void, updateShareViaException: @escaping (Bool) -> Void) {
        self.context = context
        self.isPlain = isPlain
        self.updateEditingName = updateEditingName
        self.updatePhone = updatePhone
        self.updatePhoneLabel = updatePhoneLabel
        self.deletePhone = deletePhone
        self.setPhoneIdWithRevealedOptions = setPhoneIdWithRevealedOptions
        self.addPhoneNumber = addPhoneNumber
        self.performAction = performAction
        self.toggleSelection = toggleSelection
        self.callPhone = callPhone
        self.openUrl = openUrl
        self.openAddress = openAddress
        self.displayCopyContextMenu = displayCopyContextMenu
        self.updateShareViaException = updateShareViaException
    }
}

private enum DeviceContactInfoSection: ItemListSectionId {
    case info
    case editing
    case data
    case share
}

private enum DeviceContactInfoEntryTag: Equatable, ItemListItemTag {
    case info(Int)
    case birthday
    case editingPhone(Int64)
    case note
    
    func isEqual(to other: ItemListItemTag) -> Bool {
        return self == (other as? DeviceContactInfoEntryTag)
    }
}

private enum DeviceContactInfoDataId: Hashable {
    case job
    case phoneNumber(String, String)
    case email(String, String)
    case url(String, String)
    case address(DeviceContactAddressData)
    case birthday
    case socialProfile(DeviceContactSocialProfileData)
    case instantMessenger(DeviceContactInstantMessagingProfileData)
    case note
}

private enum DeviceContactInfoConstantEntryId: Hashable {
    case info
    case invite
    case sendMessage
    case createContact
    case addToExisting
    case company
    case birthday
    case addPhoneNumber
    case phoneNumberSharingInfo
    case phoneNumberShareViaException
    case phoneNumberShareViaExceptionInfo
    case note
}

private enum DeviceContactInfoEntryId: Hashable {
    case constant(DeviceContactInfoConstantEntryId)
    case phoneNumber(Int)
    case email(Int)
    case url(Int)
    case address(Int)
    case socialProfile(Int)
    case instantMessenger(Int)
    case editingPhoneNumber(Int64)
}

private enum DeviceContactInfoEntry: ItemListNodeEntry {
    case info(Int, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, peer: Peer, state: ItemListAvatarAndNameInfoItemState, job: String?, isPlain: Bool)
    
    case invite(Int, PresentationTheme, String)
    case sendMessage(Int, PresentationTheme, String)
    case createContact(Int, PresentationTheme, String)
    case addToExisting(Int, PresentationTheme, String)
    
    case company(Int, PresentationTheme, String, String, Bool?)
    case phoneNumber(Int, Int, PresentationTheme, String, String, String, Bool?, Bool)
    case editingPhoneNumber(Int, PresentationTheme, PresentationStrings, Int64, String, String, String, Bool)
    case phoneNumberSharingInfo(Int, PresentationTheme, String)
    case phoneNumberShareViaException(Int, PresentationTheme, String, Bool)
    case phoneNumberShareViaExceptionInfo(Int, PresentationTheme, String)
    case addPhoneNumber(Int, PresentationTheme, String)
    case email(Int, Int, PresentationTheme, String, String, String, Bool?)
    case url(Int, Int, PresentationTheme, String, String, String, Bool?)
    case address(Int, Int, PresentationTheme, String, DeviceContactAddressData, Signal<(TransformImageArguments) -> DrawingContext?, NoError>?, Bool?)
    case birthday(Int, PresentationTheme, String, Date, String, Bool?)
    case socialProfile(Int, Int, PresentationTheme, String, DeviceContactSocialProfileData, String, Bool?)
    case instantMessenger(Int, Int, PresentationTheme, String, DeviceContactInstantMessagingProfileData, String, Bool?)
    case note(Int, PresentationTheme, String, String, Bool?)
    
    var section: ItemListSectionId {
        switch self {
            case .info:
                return DeviceContactInfoSection.info.rawValue
            case .editingPhoneNumber, .addPhoneNumber:
                return DeviceContactInfoSection.editing.rawValue
            case .invite, .sendMessage, .createContact, .addToExisting:
                return DeviceContactInfoSection.info.rawValue
            case .phoneNumberShareViaException, .phoneNumberShareViaExceptionInfo:
                return DeviceContactInfoSection.share.rawValue
            default:
                return DeviceContactInfoSection.data.rawValue
        }
    }
    
    var stableId: DeviceContactInfoEntryId {
        switch self {
            case .info:
                return .constant(.info)
            case .sendMessage:
                return .constant(.sendMessage)
            case .invite:
                return .constant(.invite)
            case .createContact:
                return .constant(.createContact)
            case .addToExisting:
                return .constant(.addToExisting)
            case .company:
                return .constant(.company)
            case let .phoneNumber(_, catIndex, _, _, _, _, _, _):
                return .phoneNumber(catIndex)
            case .phoneNumberSharingInfo:
                return .constant(.phoneNumberSharingInfo)
            case .phoneNumberShareViaException:
                return .constant(.phoneNumberShareViaException)
            case .phoneNumberShareViaExceptionInfo:
                return .constant(.phoneNumberShareViaExceptionInfo)
            case let .editingPhoneNumber(_, _, _, id, _, _, _, _):
                return .editingPhoneNumber(id)
            case .addPhoneNumber:
                return .constant(.addPhoneNumber)
            case let .email(_, catIndex, _, _, _, _, _):
                return .email(catIndex)
            case let .url(_, catIndex, _, _, _, _, _):
                return .url(catIndex)
            case let .address(_, catIndex, _, _, _, _, _):
                return .address(catIndex)
            case .birthday:
                return .constant(.birthday)
            case let .socialProfile(_, catIndex, _, _, _, _, _):
                return .socialProfile(catIndex)
            case let .instantMessenger(_, catIndex, _, _, _, _, _):
                return .instantMessenger(catIndex)
            case .note:
                return .constant(.note)
        }
    }
    
    static func ==(lhs: DeviceContactInfoEntry, rhs: DeviceContactInfoEntry) -> Bool {
        switch lhs {
            case let .info(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsState, lhsJobSummary, lhsIsPlain):
                if case let .info(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsState, rhsJobSummary, rhsIsPlain) = rhs {
                    if lhsIndex != rhsIndex {
                        return false
                    }
                    if lhsTheme !== rhsTheme {
                        return false
                    }
                    if lhsStrings !== rhsStrings {
                        return false
                    }
                    if lhsDateTimeFormat != rhsDateTimeFormat {
                        return false
                    }
                    if !arePeersEqual(lhsPeer, rhsPeer) {
                        return false
                    }
                    if lhsState != rhsState {
                        return false
                    }
                    if lhsJobSummary != rhsJobSummary {
                        return false
                    }
                    if lhsIsPlain != rhsIsPlain {
                        return false
                    }
                    return true
                } else {
                    return false
                }
            case let .sendMessage(lhsIndex, lhsTheme, lhsTitle):
                if case let .sendMessage(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
                    return true
                } else {
                    return false
                }
            case let .invite(lhsIndex, lhsTheme, lhsTitle):
                if case let .invite(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
                    return true
                } else {
                    return false
                }
            case let .createContact(lhsIndex, lhsTheme, lhsTitle):
                if case let .createContact(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
                    return true
                } else {
                    return false
                }
            case let .addToExisting(lhsIndex, lhsTheme, lhsTitle):
                if case let .addToExisting(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
                    return true
                } else {
                    return false
                }
            case let .company(lhsIndex, lhsTheme, lhsTitle, lhsValue, lhsSelected):
                if case let .company(rhsIndex, rhsTheme, rhsTitle, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .phoneNumber(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected, lhsIsInteractionEnabled):
                if case let .phoneNumber(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected, rhsIsInteractionEnabled) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected, lhsIsInteractionEnabled == rhsIsInteractionEnabled {
                    return true
                } else {
                    return false
                }
            case let .phoneNumberSharingInfo(lhsIndex, lhsTheme, lhsText):
                if case let .phoneNumberSharingInfo(rhsIndex, rhsTheme, rhsText) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText {
                    return true
                } else {
                    return false
                }
            case let .phoneNumberShareViaException(lhsIndex, lhsTheme, lhsText, lhsValue):
                if case let .phoneNumberShareViaException(rhsIndex, rhsTheme, rhsText, rhsValue) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
                    return true
                } else {
                    return false
                }
            case let .phoneNumberShareViaExceptionInfo(lhsIndex, lhsTheme, lhsText):
                if case let .phoneNumberShareViaExceptionInfo(rhsIndex, rhsTheme, rhsText) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsText == rhsText {
                    return true
                } else {
                    return false
                }
            case let .editingPhoneNumber(lhsIndex, lhsTheme, lhsStrings, lhsId, lhsTitle, lhsLabel, lhsValue, lhsSelected):
                if case let .editingPhoneNumber(rhsIndex, rhsTheme, rhsStrings, rhsId, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsId == rhsId, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .addPhoneNumber(lhsIndex, lhsTheme, lhsTitle):
                if case let .addPhoneNumber(rhsIndex, rhsTheme, rhsTitle) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle {
                    return true
                } else {
                    return false
                }
            case let .email(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected):
                if case let .email(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsCatIndex == rhsCatIndex, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .url(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsLabel, lhsValue, lhsSelected):
                if case let .url(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsLabel, rhsValue, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsLabel == rhsLabel, lhsValue == rhsValue, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .address(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsImageSignal, lhsSelected):
                if case let .address(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsImageSignal, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .birthday(lhsIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected):
                if case let .birthday(rhsIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .socialProfile(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected):
                if case let .socialProfile(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .instantMessenger(lhsIndex, lhsCatIndex, lhsTheme, lhsTitle, lhsValue, lhsText, lhsSelected):
                if case let .instantMessenger(rhsIndex, rhsCatIndex, rhsTheme, rhsTitle, rhsValue, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsCatIndex == rhsCatIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue, lhsText == rhsText, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
            case let .note(lhsIndex, lhsTheme, lhsTitle, lhsText, lhsSelected):
                if case let .note(rhsIndex, rhsTheme, rhsTitle, rhsText, rhsSelected) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText, lhsSelected == rhsSelected {
                    return true
                } else {
                    return false
                }
        }
    }
    
    private var sortIndex: Int {
        switch self {
            case let .info(index, _, _, _, _, _, _, _):
                return index
            case let .sendMessage(index, _, _):
                return index
            case let .invite(index, _, _):
                return index
            case let .createContact(index, _, _):
                return index
            case let .addToExisting(index, _, _):
                return index
            case let .company(index, _, _, _, _):
                return index
            case let .phoneNumber(index, _, _, _, _, _, _, _):
                return index
            case let .phoneNumberSharingInfo(index, _, _):
                return index
            case let .phoneNumberShareViaException(index, _, _, _):
                return index
            case let .phoneNumberShareViaExceptionInfo(index, _, _):
                return index
            case let .editingPhoneNumber(index, _, _, _, _, _, _, _):
                return index
            case let .addPhoneNumber(index, _, _):
                return index
            case let .email(index, _, _, _, _, _, _):
                return index
            case let .url(index, _, _, _, _, _, _):
                return index
            case let .address(index, _, _, _, _, _, _):
                return index
            case let .birthday(index, _, _, _, _, _):
                return index
            case let .socialProfile(index, _, _, _, _, _, _):
                return index
            case let .instantMessenger(index, _, _, _, _, _, _):
                return index
            case let .note(index, _, _, _, _):
                return index
        }
    }
    
    static func <(lhs: DeviceContactInfoEntry, rhs: DeviceContactInfoEntry) -> Bool {
        return lhs.sortIndex < rhs.sortIndex
    }
    
    func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
        let arguments = arguments as! DeviceContactInfoControllerArguments
        switch self {
            case let .info(_, theme, strings, dateTimeFormat, peer, state, jobSummary, isPlain):
                return ItemListAvatarAndNameInfoItem(accountContext: arguments.context, presentationData: presentationData, dateTimeFormat: dateTimeFormat, mode: .contact, peer: peer, presence: nil, label: jobSummary, cachedData: nil, state: state, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks(withTopInset: false, withExtendedBottomInset: true), editingNameUpdated: { editingName in
                    arguments.updateEditingName(editingName)
                }, avatarTapped: {
                }, context: nil, call: nil)
            case let .sendMessage(_, theme, title):
                return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: {
                    arguments.performAction(.sendMessage)
                })
            case let .invite(_, theme, title):
                return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: {
                    arguments.performAction(.invite)
                })
            case let .createContact(_, theme, title):
                return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: {
                    arguments.performAction(.createContact)
                })
            case let .addToExisting(_, theme, title):
                return ItemListActionItem(presentationData: presentationData, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, action: {
                    arguments.performAction(.addToExisting)
                })
            case let .company(_, theme, title, value, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
                }, tag: nil)
            case let .phoneNumber(_, index, theme, title, label, value, selected, isInteractionEnabled):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: isInteractionEnabled ? {
                    if selected != nil {
                        arguments.toggleSelection(.phoneNumber(label, value))
                    } else {
                        arguments.callPhone(value)
                    }
                } : nil, longTapAction: isInteractionEnabled ? {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), value)
                    }
                } : nil, tag: DeviceContactInfoEntryTag.info(index))
            case let .phoneNumberSharingInfo(_, theme, text):
                return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
            case let .phoneNumberShareViaException(_, theme, text, value):
                return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: arguments.isPlain ? .plain : .blocks, updated: { value in
                    arguments.updateShareViaException(value)
                })
            case let .phoneNumberShareViaExceptionInfo(_, theme, text):
                return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section)
            case let .editingPhoneNumber(_, theme, strings, id, title, label, value, hasActiveRevealControls):
                return UserInfoEditingPhoneItem(presentationData: presentationData, id: id, label: title, value: value, editing: UserInfoEditingPhoneItemEditing(editable: true, hasActiveRevealControls: hasActiveRevealControls), sectionId: self.section, setPhoneIdWithRevealedOptions: { lhs, rhs in
                    arguments.setPhoneIdWithRevealedOptions(lhs, rhs)
                }, updated: { value in
                    arguments.updatePhone(id, value)
                }, selectLabel: {
                    arguments.updatePhoneLabel(id, label)
                }, delete: {
                    arguments.deletePhone(id)
                }, tag: DeviceContactInfoEntryTag.editingPhone(id))
            case let .addPhoneNumber(_, theme, title):
                return UserInfoEditingPhoneActionItem(presentationData: presentationData, title: title, sectionId: self.section, action: {
                    arguments.addPhoneNumber()
                })
            case let .email(_, index, theme, title, label, value, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.email(label, value))
                    } else {
                        arguments.openUrl("mailto:\(value)")
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), value)
                    }
                }, tag: DeviceContactInfoEntryTag.info(index))
            case let .url(_, index, theme, title, label, value, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: value, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: false, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.url(label, value))
                    } else {
                        arguments.openUrl(value)
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), value)
                    }
                }, tag: DeviceContactInfoEntryTag.info(index))
            case let .address(_, index, theme, title, value, imageSignal, selected):
                var string = ""
                func combineComponent(string: inout String, component: String) {
                    if !component.isEmpty {
                        if !string.isEmpty {
                            string.append("\n")
                        }
                        string.append(component)
                    }
                }
                combineComponent(string: &string, component: value.street1)
                combineComponent(string: &string, component: value.street2)
                combineComponent(string: &string, component: value.state)
                combineComponent(string: &string, component: value.city)
                combineComponent(string: &string, component: value.country)
                combineComponent(string: &string, component: value.postcode)
                return ItemListAddressItem(theme: theme, label: title, text: string, imageSignal: imageSignal, selected: selected, sectionId: self.section, style: .plain, action: {
                    if selected != nil {
                        arguments.toggleSelection(.address(value))
                    } else {
                        arguments.openAddress(value)
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), string)
                    }
                }, tag: DeviceContactInfoEntryTag.info(index))
            case let .birthday(_, theme, title, value, text, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.birthday)
                    } else {
                        let calendar = Calendar(identifier: .gregorian)
                        var components = calendar.dateComponents([.month, .day], from: value)
                        let currentComponents = calendar.dateComponents([.year, .month, .day], from: Date())
                        
                        if let month = components.month, let currentMonth = currentComponents.month, let day = components.day, let currentDay = currentComponents.day, let currentYear = currentComponents.year {
                            if month >= currentMonth && (day >= currentDay || month > currentMonth) {
                                components.year = currentYear
                            } else {
                                components.year = currentYear + 1
                            }
                            components.hour = 12
                            components.minute = 0
                            
                            if let targetDate = calendar.date(from: components) {
                                let url = "calshow:\(targetDate.timeIntervalSinceReferenceDate)"
                                arguments.openUrl(url)
                            }
                        }
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.birthday, text)
                    }
                }, tag: DeviceContactInfoEntryTag.birthday)
            case let .socialProfile(_, index, theme, title, value, text, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.socialProfile(value))
                    } else if value.url.count > 0 {
                        arguments.openUrl(value.url)
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), text)
                    }
                }, tag: DeviceContactInfoEntryTag.info(index))
            case let .instantMessenger(_, index, theme, title, value, text, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, textColor: .accent, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.instantMessenger(value))
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.info(index), text)
                    }
                }, tag: DeviceContactInfoEntryTag.info(index))
            case let .note(_, theme, title, text, selected):
                return ItemListTextWithLabelItem(presentationData: presentationData, label: title, text: text, style: arguments.isPlain ? .plain : .blocks, enabledEntityTypes: [], multiline: true, selected: selected, sectionId: self.section, action: {
                    if selected != nil {
                        arguments.toggleSelection(.note)
                    }
                }, longTapAction: {
                    if selected == nil {
                        arguments.displayCopyContextMenu(.note, text)
                    }
                }, tag: DeviceContactInfoEntryTag.note)
        }
    }
}

private struct DeviceContactInfoEditingState: Equatable {
    var editingName: ItemListAvatarAndNameInfoItemName?
}

private struct EditingPhoneNumber: Equatable {
    var id: Int64
    var label: String
    var value: String
}

private struct DeviceContactInfoState: Equatable {
    var savingData: Bool = false
    var addToPrivacyExceptions: Bool = true
    var editingState: DeviceContactInfoEditingState? = nil
    var excludedComponents = Set<DeviceContactInfoDataId>()
    var phoneNumbers: [EditingPhoneNumber] = []
    var nextPhoneNumber: Int64 = 1
    var phoneIdWithRevealedOptions: Int64?
}

private func filteredContactData(contactData: DeviceContactExtendedData, excludedComponents: Set<DeviceContactInfoDataId>) -> DeviceContactExtendedData {
    let phoneNumbers = contactData.basicData.phoneNumbers.filter({ phoneNumber in
        return !excludedComponents.contains(.phoneNumber(phoneNumber.label, formatPhoneNumber(phoneNumber.value)))
    })
    let emailAddresses = contactData.emailAddresses.filter({ email in
        return !excludedComponents.contains(.email(email.label, email.value))
    })
    let urls = contactData.urls.filter({ url in
        return !excludedComponents.contains(.url(url.label, url.value))
    })
    let addresses = contactData.addresses.filter({ address in
        return !excludedComponents.contains(.address(address))
    })
    let socialProfiles = contactData.socialProfiles.filter({ socialProfile in
        return !excludedComponents.contains(.socialProfile(socialProfile))
    })
    let instantMessagingProfiles = contactData.instantMessagingProfiles.filter({ instantMessenger in
        return !excludedComponents.contains(.instantMessenger(instantMessenger))
    })
    let includeJob = !excludedComponents.contains(.job)
    let includeBirthday = !excludedComponents.contains(.birthday)
    let includeNote = !excludedComponents.contains(.note)
    return DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contactData.basicData.firstName, lastName: contactData.basicData.lastName, phoneNumbers: phoneNumbers), middleName: contactData.middleName, prefix: contactData.prefix, suffix: contactData.suffix, organization: includeJob ? contactData.organization : "", jobTitle: includeJob ? contactData.jobTitle : "", department: includeJob ? contactData.department : "", emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: includeBirthday ? contactData.birthdayDate : nil, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles, note: includeNote ? contactData.note : "")
}

private func deviceContactInfoEntries(account: Account, presentationData: PresentationData, peer: Peer?, isShare: Bool, shareViaException: Bool, contactData: DeviceContactExtendedData, isContact: Bool, state: DeviceContactInfoState, selecting: Bool, editingPhoneNumbers: Bool) -> [DeviceContactInfoEntry] {
    var entries: [DeviceContactInfoEntry] = []
    
    var editingName: ItemListAvatarAndNameInfoItemName?
    
    if let editingState = state.editingState {
        editingName = editingState.editingName
    }
    
    var personName: (String, String) = (contactData.basicData.firstName, contactData.basicData.lastName)
    if let editingName = editingName {
        switch editingName {
        case let .personName(firstName, lastName, _):
            personName = (firstName, lastName)
        default:
            break
        }
    }
    
    var jobComponents: [String] = []
    if !contactData.organization.isEmpty {
        jobComponents.append(contactData.organization)
    }
    if !contactData.department.isEmpty {
        jobComponents.append(contactData.department)
    }
    if !contactData.jobTitle.isEmpty {
        jobComponents.append(contactData.jobTitle)
    }
    let jobSummary = jobComponents.joined(separator: " — ")
    
    let isOrganization = personName.0.isEmpty && personName.1.isEmpty && !contactData.organization.isEmpty
    
    entries.append(.info(entries.count, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer: peer ?? TelegramUser(id: PeerId(namespace: -1, id: 0), accessHash: nil, firstName: isOrganization ? contactData.organization : personName.0, lastName: isOrganization ? nil : personName.1, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []), state: ItemListAvatarAndNameInfoItemState(editingName: editingName, updatingName: nil), job: isOrganization ? nil : jobSummary, isPlain: !isShare))
    
    if !selecting {
        if let _ = peer {
            entries.append(.sendMessage(entries.count, presentationData.theme, presentationData.strings.UserInfo_SendMessage))
        } else {
            entries.append(.invite(entries.count, presentationData.theme, presentationData.strings.Contacts_InviteToTelegram))
        }
        
        if !isContact {
            entries.append(.createContact(entries.count, presentationData.theme, presentationData.strings.UserInfo_CreateNewContact))
            entries.append(.addToExisting(entries.count, presentationData.theme, presentationData.strings.UserInfo_AddToExisting))
        }
    }
    
    if isShare {
        var numberIndex = 0
        if contactData.basicData.phoneNumbers.isEmpty {
            entries.append(.phoneNumber(entries.count, numberIndex, presentationData.theme, localizedPhoneNumberLabel(label: "_$!<Mobile>!$_", strings: presentationData.strings), localizedPhoneNumberLabel(label: "_$!<Mobile>!$_", strings: presentationData.strings), presentationData.strings.ContactInfo_PhoneNumberHidden, nil, false))
            numberIndex += 1
        }
        for number in contactData.basicData.phoneNumbers {
            let formattedNumber = formatPhoneNumber(number.value)
            entries.append(.phoneNumber(entries.count, numberIndex, presentationData.theme, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.label, formattedNumber, nil, false))
            numberIndex += 1
        }
        if let peer = peer {
            let personCompactName: String
            if !personName.0.isEmpty {
                personCompactName = personName.0
            } else if !personName.1.isEmpty {
                personCompactName = personName.1
            } else {
                personCompactName = peer.compactDisplayTitle
            }
            
            if contactData.basicData.phoneNumbers.isEmpty {
                entries.append(.phoneNumberSharingInfo(entries.count, presentationData.theme, presentationData.strings.AddContact_ContactWillBeSharedAfterMutual(personCompactName).0))
            }
            if shareViaException {
                entries.append(.phoneNumberShareViaException(entries.count, presentationData.theme, presentationData.strings.AddContact_SharedContactException, state.addToPrivacyExceptions))
                entries.append(.phoneNumberShareViaExceptionInfo(entries.count, presentationData.theme, presentationData.strings.AddContact_SharedContactExceptionInfo(personCompactName).0))
            }
        }
    } else {
        if editingPhoneNumbers {
            for number in state.phoneNumbers {
                let label = !number.label.isEmpty ? number.label : "_$!<Mobile>!$_"
                entries.append(.editingPhoneNumber(entries.count, presentationData.theme, presentationData.strings, number.id, localizedPhoneNumberLabel(label: label, strings: presentationData.strings), label, number.value, state.phoneIdWithRevealedOptions == number.id))
            }
            entries.append(.addPhoneNumber(entries.count, presentationData.theme, presentationData.strings.UserInfo_AddPhone))
        } else {
            var numberIndex = 0
            for number in contactData.basicData.phoneNumbers {
                let formattedNumber = formatPhoneNumber(number.value)
                entries.append(.phoneNumber(entries.count, numberIndex, presentationData.theme, localizedPhoneNumberLabel(label: number.label, strings: presentationData.strings), number.label, formattedNumber, selecting ? !state.excludedComponents.contains(.phoneNumber(number.label, formattedNumber)) : nil, true))
                numberIndex += 1
            }
        }
    }
    
    var emailIndex = 0
    for email in contactData.emailAddresses {
        entries.append(.email(entries.count, emailIndex, presentationData.theme, localizedGenericContactFieldLabel(label: email.label, strings: presentationData.strings), email.label, email.value, selecting ? !state.excludedComponents.contains(.email(email.label, email.value)) : nil))
        emailIndex += 1
    }
    
    var urlIndex = 0
    for url in contactData.urls {
        entries.append(.url(entries.count, urlIndex, presentationData.theme, localizedGenericContactFieldLabel(label: url.label, strings: presentationData.strings), url.label, url.value, selecting ? !state.excludedComponents.contains(.url(url.label, url.value)) : nil))
        urlIndex += 1
    }
    
    var addressIndex = 0
    for address in contactData.addresses {
        let signal = geocodeLocation(dictionary: address.dictionary)
        |> mapToSignal { coordinates -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
            if let (latitude, longitude) = coordinates {
                let resource = MapSnapshotMediaResource(latitude: latitude, longitude: longitude, width: 90, height: 90)
                return chatMapSnapshotImage(account: account, resource: resource)
            } else {
                return .single({ _ in return nil })
            }
        }
        
        entries.append(.address(entries.count, addressIndex, presentationData.theme, localizedGenericContactFieldLabel(label: address.label, strings: presentationData.strings), address, signal, selecting ? !state.excludedComponents.contains(.address(address)) : nil))
        addressIndex += 1
    }
    
    if let birthday = contactData.birthdayDate {
        let dateText: String
        let calendar = Calendar(identifier: .gregorian)
        let components = calendar.dateComponents(Set([.era, .year, .month, .day]), from: birthday)
        if let year = components.year, year > 1 {
            dateText = stringForDate(date: birthday, timeZone: TimeZone.current, strings: presentationData.strings)
        } else {
            dateText = stringForDateWithoutYear(date: birthday, timeZone: TimeZone.current, strings: presentationData.strings)
        }
        entries.append(.birthday(entries.count, presentationData.theme, presentationData.strings.ContactInfo_BirthdayLabel, birthday, dateText, selecting ? !state.excludedComponents.contains(.birthday) : nil))
    }
    
    var socialProfileIndex = 0
    for profile in contactData.socialProfiles {
        var label = localizedGenericContactFieldLabel(label: profile.label, strings: presentationData.strings)
        var text = profile.username
        switch profile.service.lowercased() {
            case "twitter":
                label = "Twitter"
                text = "@\(profile.username)"
            case "facebook":
                label = "Facebook"
            default:
                if !profile.service.isEmpty {
                    label = profile.service
                }
        }
        entries.append(.socialProfile(entries.count, socialProfileIndex, presentationData.theme, label, profile, text, selecting ? !state.excludedComponents.contains(.socialProfile(profile)) : nil))
        socialProfileIndex += 1
    }
    
    var instantMessagingProfileIndex = 0
    for profile in contactData.instantMessagingProfiles {
        var label = localizedGenericContactFieldLabel(label: profile.label, strings: presentationData.strings)
        if !profile.service.isEmpty {
            label = profile.service
        }
        entries.append(.instantMessenger(entries.count, instantMessagingProfileIndex, presentationData.theme, label, profile, profile.username, selecting ? !state.excludedComponents.contains(.instantMessenger(profile)) : nil))
        instantMessagingProfileIndex += 1
    }
    
    if !contactData.note.isEmpty {
        entries.append(.note(entries.count, presentationData.theme, presentationData.strings.ContactInfo_Note, contactData.note, selecting ? !state.excludedComponents.contains(.note) : nil))
    }
    
    return entries
}

private final class DeviceContactInfoController: ItemListController, MFMessageComposeViewControllerDelegate, UINavigationControllerDelegate {
    private var composer: MFMessageComposeViewController?
    func inviteContact(presentationData: PresentationData, numbers: [String]) {
        if MFMessageComposeViewController.canSendText() {
            let composer = MFMessageComposeViewController()
            composer.messageComposeDelegate = self
            composer.recipients = Array(Set(numbers))
            let url = presentationData.strings.InviteText_URL
            let body = presentationData.strings.InviteText_SingleContact(url).0
            composer.body = body
            self.composer = composer
            if let window = self.view.window {
                window.rootViewController?.present(composer, animated: true)
            }
        }
    }
    
    @objc func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
        self.composer = nil
        
        controller.dismiss(animated: true, completion: nil)
        
        guard case .sent = result else {
            return
        }
    }
}

public func deviceContactInfoController(context: AccountContext, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController {
    var initialState = DeviceContactInfoState()
    if case let .create(peer, contactData, _, _, _) = subject {
        var peerPhoneNumber: String?
        var firstName = contactData.basicData.firstName
        var lastName = contactData.basicData.lastName
        if let peer = peer as? TelegramUser {
            firstName = peer.firstName ?? ""
            lastName = peer.lastName ?? ""
            if let phone = peer.phone {
                let formattedPhone = formatPhoneNumber(phone)
                peerPhoneNumber = formattedPhone
                initialState.phoneNumbers.append(EditingPhoneNumber(id: initialState.nextPhoneNumber, label: "_$!<Mobile>!$_", value: formattedPhone))
                initialState.nextPhoneNumber += 1
            }
        }
        for phoneNumber in contactData.basicData.phoneNumbers {
            if peerPhoneNumber != formatPhoneNumber(phoneNumber.value) {
                initialState.phoneNumbers.append(EditingPhoneNumber(id: initialState.nextPhoneNumber, label: phoneNumber.label, value: phoneNumber.value))
                initialState.nextPhoneNumber += 1
            }
        }
        initialState.editingState = DeviceContactInfoEditingState(editingName: .personName(firstName: firstName, lastName: lastName, phone: ""))
    }
    let statePromise = ValuePromise(initialState, ignoreRepeated: true)
    let stateValue = Atomic(value: initialState)
    let updateState: ((DeviceContactInfoState) -> DeviceContactInfoState) -> Void = { f in
        statePromise.set(stateValue.modify { f($0) })
    }
    
    var addToExistingImpl: (() -> Void)?
    var openChatImpl: ((PeerId) -> Void)?
    var replaceControllerImpl: ((ViewController) -> Void)?
    var pushControllerImpl: ((ViewController) -> Void)?
    var presentControllerImpl: ((ViewController, Any?) -> Void)?
    var openUrlImpl: ((String) -> Void)?
    var openAddressImpl: ((DeviceContactAddressData) -> Void)?
    var inviteImpl: (([String]) -> Void)?
    var dismissImpl: ((Bool) -> Void)?
    
    let actionsDisposable = DisposableSet()
    
    let addContactDisposable = MetaDisposable()
    actionsDisposable.add(addContactDisposable)
    
    var displayCopyContextMenuImpl: ((DeviceContactInfoEntryTag, String) -> Void)?
    
    let callImpl: (String) -> Void = { number in
        let _ = (context.account.postbox.transaction { transaction -> TelegramUser? in
            if let peer = subject.peer {
                return transaction.getPeer(peer.id) as? TelegramUser
            }
            return nil
        }
        |> deliverOnMainQueue).start(next: { user in
            if let user = user, let phone = user.phone, formatPhoneNumber(phone) == formatPhoneNumber(number) {
                let presentationData = context.sharedContext.currentPresentationData.with { $0 }
                let controller = ActionSheetController(presentationData: presentationData)
                let dismissAction: () -> Void = { [weak controller] in
                    controller?.dismissAnimated()
                }
                controller.setItemGroups([
                    ActionSheetItemGroup(items: [
                        ActionSheetButtonItem(title: presentationData.strings.UserInfo_TelegramCall, action: {
                            dismissAction()
                            let callResult = context.sharedContext.callManager?.requestCall(account: context.account, peerId: user.id, endCurrentIfAny: false)
                            if let callResult = callResult, case let .alreadyInProgress(currentPeerId) = callResult {
                                if currentPeerId == user.id {
                                    context.sharedContext.navigateToCurrentCall()
                                } else {
                                    let presentationData = context.sharedContext.currentPresentationData.with { $0 }
                                    let _ = (context.account.postbox.transaction { transaction -> (Peer?, Peer?) in
                                        return (transaction.getPeer(user.id), transaction.getPeer(currentPeerId))
                                        } |> deliverOnMainQueue).start(next: { peer, current in
                                            if let peer = peer, let current = current {
                                                presentControllerImpl?(textAlertController(context: context, title: presentationData.strings.Call_CallInProgressTitle, text: presentationData.strings.Call_CallInProgressMessage(current.compactDisplayTitle, peer.compactDisplayTitle).0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
                                                    let _ = context.sharedContext.callManager?.requestCall(account: context.account, peerId: peer.id, endCurrentIfAny: true)
                                                })]), nil)
                                            }
                                        })
                                }
                            }
                        }),
                        ActionSheetButtonItem(title: presentationData.strings.UserInfo_PhoneCall, action: {
                            dismissAction()
                            context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))")
                        }),
                    ]),
                    ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
                ])
                presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
            } else {
                context.sharedContext.applicationBindings.openUrl("tel:\(formatPhoneNumber(number).replacingOccurrences(of: " ", with: ""))")
            }
        })
    }
    
    let contactData: Signal<(Peer?, DeviceContactStableId?, DeviceContactExtendedData), NoError>
    var isShare = false
    var shareViaException = false
    switch subject {
    case let .vcard(peer, id, data):
        contactData = .single((peer, id, data))
    case let .filter(peer, id, data, _):
        contactData = .single((peer, id, data))
    case let .create(peer, data, share, shareViaExceptionValue, _):
        contactData = .single((peer, nil, data))
        isShare = share
        shareViaException = shareViaExceptionValue
    }
    
    let arguments = DeviceContactInfoControllerArguments(context: context, isPlain: !isShare, updateEditingName: { editingName in
        updateState { state in
            var state = state
            if let _ = state.editingState {
                state.editingState = DeviceContactInfoEditingState(editingName: editingName)
            }
            return state
        }
    }, updatePhone: { id, value in
        updateState { state in
            var state = state
            for i in 0 ..< state.phoneNumbers.count {
                if state.phoneNumbers[i].id == id {
                    state.phoneNumbers[i].value = value
                    break
                }
            }
            return state
        }
    }, updatePhoneLabel: { id, currentLabel in
        pushControllerImpl?(phoneLabelController(context: context, currentLabel: currentLabel, completion: { value in
            updateState { state in
                var state = state
                for i in 0 ..< state.phoneNumbers.count {
                    if state.phoneNumbers[i].id == id {
                        state.phoneNumbers[i].label = value
                        break
                    }
                }
                return state
            }
        }))
    }, deletePhone: { id in
        updateState { state in
            var state = state
            for i in 0 ..< state.phoneNumbers.count {
                if state.phoneNumbers[i].id == id {
                    state.phoneNumbers.remove(at: i)
                    break
                }
            }
            return state
        }
    }, setPhoneIdWithRevealedOptions: { id, fromId in
        updateState { state in
            var state = state
            if (id == nil && fromId == state.phoneIdWithRevealedOptions) || (id != nil && fromId == nil) {
                state.phoneIdWithRevealedOptions = id
            }
            return state
        }
    }, addPhoneNumber: {
        updateState { state in
            var state = state
            let id = state.nextPhoneNumber
            state.nextPhoneNumber += 1
            state.phoneNumbers.append(EditingPhoneNumber(id: id, label: "_$!<Mobile>!$_", value: "+"))
            return state
        }
    }, performAction: { action in
        switch action {
            case .invite:
                let inviteAction: (String) -> Void = { number in
                    inviteImpl?([number])
                }
                if subject.contactData.basicData.phoneNumbers.count == 1 {
                    inviteAction(subject.contactData.basicData.phoneNumbers[0].value)
                } else {
                    let presentationData = context.sharedContext.currentPresentationData.with { $0 }
                    let controller = ActionSheetController(presentationData: presentationData)
                    let dismissAction: () -> Void = { [weak controller] in
                        controller?.dismissAnimated()
                    }
                    var items: [ActionSheetItem] = []
                    for phoneNumber in subject.contactData.basicData.phoneNumbers {
                        items.append(ActionSheetButtonItem(title: formatPhoneNumber(phoneNumber.value), action: {
                            dismissAction()
                            inviteAction(phoneNumber.value)
                        }))
                    }
                    
                    controller.setItemGroups([
                        ActionSheetItemGroup(items: items),
                        ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
                    ])
                    presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
                }
            case .createContact:
                pushControllerImpl?(deviceContactInfoController(context: context, subject: .create(peer: subject.peer, contactData: subject.contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in
                    dismissImpl?(false)
                    if let peer = peer {
                        
                    } else {
                        
                    }
                }), completed: nil, cancelled: nil))
            case .addToExisting:
                addToExistingImpl?()
            case .sendMessage:
                if let peer = subject.peer {
                    openChatImpl?(peer.id)
                }
        }
    }, toggleSelection: { dataId in
        updateState { state in
            var state = state
            if state.excludedComponents.contains(dataId) {
                state.excludedComponents.remove(dataId)
            } else {
                state.excludedComponents.insert(dataId)
            }
            return state
        }
    }, callPhone: { phoneNumber in
        callImpl(phoneNumber)
    }, openUrl: { url in
        openUrlImpl?(url)
    }, openAddress: { address in
        openAddressImpl?(address)
    }, displayCopyContextMenu: { tag, value in
        displayCopyContextMenuImpl?(tag, value)
    }, updateShareViaException: { value in
        updateState { state in
            var state = state
            state.addToPrivacyExceptions = value
            return state
        }
    })
    
    let previousEditingPhoneIds = Atomic<Set<Int64>?>(value: nil)
    let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), contactData)
    |> map { presentationData, state, peerAndContactData -> (ItemListControllerState, (ItemListNodeState, Any)) in
        var leftNavigationButton: ItemListNavigationButton?
        switch subject {
            case .vcard:
                break
            case .filter, .create:
                leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
                    dismissImpl?(true)
                    cancelled?()
                })
        }
        
        var rightNavigationButton: ItemListNavigationButton?
        if state.savingData {
            rightNavigationButton = ItemListNavigationButton(content: .none, style: .activity, enabled: true, action: {})
        } else if case let .filter(_, _, _, completion) = subject {
            let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents)
            rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.ShareMenu_Send), style: .bold, enabled: !filteredData.basicData.phoneNumbers.isEmpty, action: {
                completion(peerAndContactData.0, filteredData)
                dismissImpl?(true)
            })
        } else if case let .create(createForPeer, _, _, _, completion) = subject {
            let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents)
            var filteredPhoneNumbers: [DeviceContactPhoneNumberData] = []
            for phoneNumber in state.phoneNumbers {
                if !phoneNumber.value.isEmpty && phoneNumber.value != "+" {
                    filteredPhoneNumbers.append(DeviceContactPhoneNumberData(label: phoneNumber.label, value: phoneNumber.value))
                }
            }
            var composedContactData: DeviceContactExtendedData?
            if let editingName = state.editingState?.editingName, case let .personName(firstName, lastName, _) = editingName, (!firstName.isEmpty || !lastName.isEmpty) {
                var urls = filteredData.urls
                if let createForPeer = createForPeer {
                    let appProfile = DeviceContactUrlData(appProfile: createForPeer.id)
                    var found = false
                    for url in urls {
                        if url.label == appProfile.label && url.value == appProfile.value {
                            found = true
                            break
                        }
                    }
                    if !found {
                        urls.append(appProfile)
                    }
                }
                composedContactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: firstName, lastName: lastName, phoneNumbers: filteredPhoneNumbers), middleName: filteredData.middleName, prefix: filteredData.prefix, suffix: filteredData.suffix, organization: filteredData.organization, jobTitle: filteredData.jobTitle, department: filteredData.department, emailAddresses: filteredData.emailAddresses, urls: urls, addresses: filteredData.addresses, birthdayDate: filteredData.birthdayDate, socialProfiles: filteredData.socialProfiles, instantMessagingProfiles: filteredData.instantMessagingProfiles, note: filteredData.note)
            }
            rightNavigationButton = ItemListNavigationButton(content: .text(isShare ? presentationData.strings.Common_Done : presentationData.strings.Compose_Create), style: .bold, enabled: (isShare || !filteredPhoneNumbers.isEmpty) && composedContactData != nil, action: {
                if let composedContactData = composedContactData {
                    var addToPrivacyExceptions = false
                    updateState { state in
                        var state = state
                        state.savingData = true
                        addToPrivacyExceptions = state.addToPrivacyExceptions
                        return state
                    }
                    if let contactDataManager = context.sharedContext.contactDataManager {
                        switch subject {
                            case let .create(peer, _, share, shareViaException, _):
                                if share, filteredPhoneNumbers.count <= 1, let peer = peer {
                                    addContactDisposable.set((addContactInteractively(account: context.account, peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions)
                                    |> deliverOnMainQueue).start(error: { _ in
                                        presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
                                    }, completed: {
                                        let _ = (contactDataManager.createContactWithData(composedContactData)
                                        |> deliverOnMainQueue).start(next: { contactIdAndData in
                                            updateState { state in
                                                var state = state
                                                state.savingData = false
                                                return state
                                            }
                                            if let contactIdAndData = contactIdAndData {
                                                completion(peer, contactIdAndData.0, contactIdAndData.1)
                                            }
                                            completed?()
                                            dismissImpl?(true)
                                        })
                                    }))
                                    return
                                }
                            default:
                                break
                        }
                        
                        let _ = (contactDataManager.createContactWithData(composedContactData)
                        |> castError(AddContactError.self)
                        |> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
                            guard let (id, data) = contactIdAndData else {
                                return .single(nil)
                            }
                            if filteredPhoneNumbers.count <= 1 {
                                switch subject {
                                    case let .create(peer, _, share, shareViaException, _):
                                        if share, let peer = peer {
                                            return addContactInteractively(account: context.account, peerId: peer.id, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers.first?.value ?? "", addToPrivacyExceptions: shareViaException && addToPrivacyExceptions)
                                            |> mapToSignal { _ -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
                                                return .complete()
                                            }
                                            |> then(
                                                context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
                                                    return (id, data, transaction.getPeer(peer.id))
                                                }
                                                |> castError(AddContactError.self)
                                            )
                                        }
                                    default:
                                        break
                                }
                                
                                return importContact(account: context.account, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value)
                                |> castError(AddContactError.self)
                                |> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
                                    if let peerId = peerId {
                                        return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
                                            return (id, data, transaction.getPeer(peerId))
                                        }
                                        |> castError(AddContactError.self)
                                    } else {
                                        return .single((id, data, nil))
                                    }
                                }
                            } else {
                                return .single((id, data, nil))
                            }
                        }
                        |> deliverOnMainQueue).start(next: { contactIdAndData in
                            updateState { state in
                                var state = state
                                state.savingData = false
                                return state
                            }
                            if let contactIdAndData = contactIdAndData {
                                completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1)
                            }
                            completed?()
                            dismissImpl?(true)
                        })
                    }
                }
            })
        }
        
        var editingPhones = false
        var selecting = false
        let title: String
        switch subject {
            case .vcard:
                title = presentationData.strings.UserInfo_Title
            case .filter:
                selecting = true
                title = presentationData.strings.UserInfo_Title
            case .create:
                selecting = true
                editingPhones = true
                title = presentationData.strings.NewContact_Title
        }
        if case .filter = subject {
            selecting = true
        }
        
        let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: nil)
        
        let editingPhoneIds = Set<Int64>(state.phoneNumbers.map({ $0.id }))
        let previousPhoneIds = previousEditingPhoneIds.swap(editingPhoneIds)
        let insertedPhoneIds = editingPhoneIds.subtracting(previousPhoneIds ?? Set())
        var insertedPhoneId: Int64?
        if insertedPhoneIds.count == 1, let id = insertedPhoneIds.first {
            for phoneNumber in state.phoneNumbers {
                if phoneNumber.id == id {
                    if phoneNumber.value.isEmpty || phoneNumber.value == "+" {
                        insertedPhoneId = id
                    }
                    break
                }
            }
        }
        
        var focusItemTag: ItemListItemTag?
        if let insertedPhoneId = insertedPhoneId {
            focusItemTag = DeviceContactInfoEntryTag.editingPhone(insertedPhoneId)
        }
        
        let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: deviceContactInfoEntries(account: context.account, presentationData: presentationData, peer: peerAndContactData.0, isShare: isShare, shareViaException: shareViaException, contactData: peerAndContactData.2, isContact: peerAndContactData.1 != nil, state: state, selecting: selecting, editingPhoneNumbers: editingPhones), style: isShare ? .blocks : .plain, focusItemTag: focusItemTag)
        
        return (controllerState, (listState, arguments))
    }
    |> afterDisposed {
        actionsDisposable.dispose()
    }
    
    let controller = DeviceContactInfoController(context: context, state: signal)
    controller.navigationPresentation = .modal
    addToExistingImpl = { [weak controller] in
        guard let controller = controller else {
            return
        }
        addContactToExisting(context: context, parentController: controller, contactData: subject.contactData, completion: { peer, contactId, contactData in
            replaceControllerImpl?(deviceContactInfoController(context: context, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil))
        })
    }
    openChatImpl = { [weak controller] peerId in
        if let navigationController = (controller?.navigationController as? NavigationController) {
            context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peerId)))
        }
    }
    replaceControllerImpl = { [weak controller] value in
        (controller?.navigationController as? NavigationController)?.replaceTopController(value, animated: true)
    }
    pushControllerImpl = { [weak controller] c in
        (controller?.navigationController as? NavigationController)?.pushViewController(c)
    }
    presentControllerImpl = { [weak controller] value, presentationArguments in
        controller?.present(value, in: .window(.root), with: presentationArguments)
    }
    dismissImpl = { [weak controller] animated in
        guard let controller = controller else {
            return
        }
        controller.view.endEditing(true)
        if let navigationController = controller.navigationController as? NavigationController {
            navigationController.filterController(controller, animated: animated)
        } else {
            controller.dismiss()
        }
    }
    inviteImpl = { [weak controller] numbers in
        controller?.inviteContact(presentationData: context.sharedContext.currentPresentationData.with { $0 }, numbers: numbers)
    }
    openAddressImpl = { [weak controller] address in
        guard let _ = controller else {
            return
        }
    }
    openUrlImpl = { [weak controller] url in
        guard let controller = controller else {
            return
        }
        context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: context.sharedContext.currentPresentationData.with { $0 }, navigationController: controller.navigationController as? NavigationController, dismissInput: { [weak controller] in
            controller?.view.endEditing(true)
        })
    }
    
    displayCopyContextMenuImpl = { [weak controller] tag, value in
        if let strongController = controller {
            let presentationData = context.sharedContext.currentPresentationData.with { $0 }
            var resultItemNode: ListViewItemNode?
            let _ = strongController.frameForItemNode({ itemNode in
                if let itemNode = itemNode as? ItemListTextWithLabelItemNode {
                    if let itemTag = itemNode.tag as? DeviceContactInfoEntryTag {
                        if itemTag == tag && itemNode.item?.text == value {
                            resultItemNode = itemNode
                            return true
                        }
                    }
                }
                return false
            })
            if let resultItemNode = resultItemNode {
                let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: {
                    UIPasteboard.general.string = value
                })])
                strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in
                    if let strongController = controller, let resultItemNode = resultItemNode {
                        return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds)
                    } else {
                        return nil
                    }
                }))
                
            }
        }
    }
    
    return controller
}

private func addContactToExisting(context: AccountContext, parentController: ViewController, contactData: DeviceContactExtendedData, completion: @escaping (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) {
    let contactsController = context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams(context: context, title: { $0.Contacts_Title }, displayDeviceContacts: true))
    contactsController.navigationPresentation = .modal
    (parentController.navigationController as? NavigationController)?.pushViewController(contactsController)
    let _ = (contactsController.result
    |> deliverOnMainQueue).start(next: { peer in
        if let peer = peer {
            let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError>
            switch peer {
                case let .peer(contact, _, _):
                    guard let contact = contact as? TelegramUser, let phoneNumber = contact.phone else {
                        return
                    }
                    dataSignal = (context.sharedContext.contactDataManager?.basicData() ?? .single([:]))
                    |> take(1)
                    |> mapToSignal { basicData -> Signal<(Peer?, DeviceContactStableId?), NoError> in
                        var stableId: String?
                        let queryPhoneNumber = formatPhoneNumber(phoneNumber)
                        outer: for (id, data) in basicData {
                            for phoneNumber in data.phoneNumbers {
                                if formatPhoneNumber(phoneNumber.value) == queryPhoneNumber {
                                    stableId = id
                                    break outer
                                }
                            }
                        }
                        return .single((contact, stableId))
                    }
                case let .deviceContact(id, _):
                    dataSignal = .single((nil, id))
            }
            let _ = (dataSignal
            |> deliverOnMainQueue).start(next: { peer, stableId in
                guard let stableId = stableId else {
                    parentController.present(deviceContactInfoController(context: context, subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in
                        
                    }), completed: nil, cancelled: nil), in: .window(.root))
                    return
                }
                if let contactDataManager = context.sharedContext.contactDataManager {
                    let _ = (contactDataManager.appendContactData(contactData, to: stableId)
                    |> deliverOnMainQueue).start(next: { contactData in
                        guard let contactData = contactData else {
                            return
                        }
                        let _ = (context.account.postbox.contactPeersView(accountPeerId: nil, includePresences: false)
                        |> take(1)
                        |> deliverOnMainQueue).start(next: { view in
                            let phones = Set<String>(contactData.basicData.phoneNumbers.map {
                                return formatPhoneNumber($0.value)
                            })
                            var foundPeer: Peer?
                            for peer in view.peers {
                                if let user = peer as? TelegramUser, let phone = user.phone {
                                    let phone = formatPhoneNumber(phone)
                                    if phones.contains(phone) {
                                        foundPeer = peer
                                        break
                                    }
                                }
                            }
                            completion(foundPeer, stableId, contactData)
                        })
                    })
                }
            })
        }
    })
}

func addContactOptionsController(context: AccountContext, peer: Peer?, contactData: DeviceContactExtendedData) -> ActionSheetController {
    let presentationData = context.sharedContext.currentPresentationData.with { $0 }
    let controller = ActionSheetController(presentationData: presentationData)
    let dismissAction: () -> Void = { [weak controller] in
        controller?.dismissAnimated()
    }
    
    controller.setItemGroups([
        ActionSheetItemGroup(items: [
            ActionSheetButtonItem(title: presentationData.strings.Profile_CreateNewContact, action: { [weak controller] in
                controller?.present(context.sharedContext.makeDeviceContactInfoController(context: context, subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { peer, stableId, contactData in
                    if let peer = peer {
                        
                    } else {
                        
                    }
                }), completed: nil, cancelled: nil), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
                dismissAction()
            }),
            ActionSheetButtonItem(title: presentationData.strings.Profile_AddToExisting, action: { [weak controller] in
                guard let controller = controller else {
                    return
                }
                addContactToExisting(context: context, parentController: controller, contactData: contactData, completion: { peer, contactId, contactData in
                    
                })
                dismissAction()
            })
        ]),
        ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
    ])
    return controller
}