import Foundation
import UIKit
import AppBundle
import AsyncDisplayKit
import Display
import SwiftSignalKit
import AlertUI
import OverlayStatusController
import WalletUrl
import WalletCore
import Markdown

private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()

private final class WalletSendScreenArguments {
    let context: WalletContext
    let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void
    let updateText: (WalletSendScreenEntryTag, String) -> Void
    let selectNextInputItem: (WalletSendScreenEntryTag) -> Void
    let scrollToBottom: () -> Void
    let dismissInput: () -> Void
    let openQrScanner: () -> Void
    let proceed: () -> Void
    
    init(context: WalletContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, updateText: @escaping (WalletSendScreenEntryTag, String) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, scrollToBottom: @escaping () -> Void, dismissInput: @escaping () -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) {
        self.context = context
        self.updateState = updateState
        self.updateText = updateText
        self.selectNextInputItem = selectNextInputItem
        self.scrollToBottom = scrollToBottom
        self.dismissInput = dismissInput
        self.openQrScanner = openQrScanner
        self.proceed = proceed
    }
}

private enum WalletSendScreenSection: Int32 {
    case address
    case amount
    case comment
}

private enum WalletSendScreenEntryTag: ItemListItemTag {
    case address
    case amount
    case comment
    
    func isEqual(to other: ItemListItemTag) -> Bool {
        if let other = other as? WalletSendScreenEntryTag {
            return self == other
        } else {
            return false
        }
    }
}

private enum WalletSendScreenEntry: ItemListNodeEntry {
    case addressHeader(WalletTheme, String)
    case address(WalletTheme, String, String)
    case addressInfo(WalletTheme, String)
    case amountHeader(WalletTheme, String, String?, Bool)
    case amount(WalletTheme, WalletStrings, String, String)
    case commentHeader(WalletTheme, String)
    case comment(WalletTheme, String, String)
    
    var section: ItemListSectionId {
        switch self {
        case .addressHeader, .address, .addressInfo:
            return WalletSendScreenSection.address.rawValue
        case .amountHeader, .amount:
            return WalletSendScreenSection.amount.rawValue
        case .commentHeader, .comment:
            return WalletSendScreenSection.comment.rawValue
        }
    }
    
    var stableId: Int32 {
        switch self {
        case .addressHeader:
            return 0
        case .address:
            return 1
        case .addressInfo:
            return 2
        case .amountHeader:
            return 3
        case .amount:
            return 4
        case .commentHeader:
            return 5
        case .comment:
            return 6
        }
    }
    
    static func ==(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool {
        switch lhs {
        case let .addressHeader(lhsTheme, lhsText):
            if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
                return true
            } else {
                return false
            }
        case let .address(lhsTheme, lhsPlaceholder, lhsAddress):
            if case let .address(rhsTheme, rhsPlaceholder, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsAddress == rhsAddress {
                return true
            } else {
                return false
            }
        case let .addressInfo(lhsTheme, lhsText):
            if case let .addressInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
                return true
            } else {
                return false
            }
        case let .amountHeader(lhsTheme, lhsText, lhsBalance, lhsInsufficient):
            if case let .amountHeader(rhsTheme, rhsText, rhsBalance, rhsInsufficient) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsBalance == rhsBalance, lhsInsufficient == rhsInsufficient {
                return true
            } else {
                return false
            }
        case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsAmount):
            if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsAmount == rhsAmount {
                return true
            } else {
                return false
            }
        case let .commentHeader(lhsTheme, lhsText):
            if case let .commentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
                return true
            } else {
                return false
            }
        case let .comment(lhsTheme, lhsPlaceholder, lhsText):
            if case let .comment(rhsTheme, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText {
                return true
            } else {
                return false
            }
        }
    }
    
    static func <(lhs: WalletSendScreenEntry, rhs: WalletSendScreenEntry) -> Bool {
        return lhs.stableId < rhs.stableId
    }
    
    func item(_ arguments: Any) -> ListViewItem {
        let arguments = arguments as! WalletSendScreenArguments
        switch self {
        case let .addressHeader(theme, text):
            return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
        case let .address(theme, placeholder, address):
            return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { text in
                arguments.updateText(WalletSendScreenEntryTag.address, text.replacingOccurrences(of: "\n", with: ""))
            }, shouldUpdateText: { text in
                return isValidAddress(text)
            }, processPaste: { text in
                if let url = URL(string: text), let parsedUrl = parseWalletUrl(url) {
                    var focusItemTag: WalletSendScreenEntryTag?
                    arguments.updateState { state in
                        var state = state
                        state.address = parsedUrl.address
                        if let amount = parsedUrl.amount {
                            state.amount = formatBalanceText(amount, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
                        } else if state.amount.isEmpty {
                            focusItemTag = WalletSendScreenEntryTag.address
                        }
                        if let comment = parsedUrl.comment {
                            state.comment = comment
                        } else if state.comment.isEmpty && focusItemTag == nil {
                            focusItemTag = WalletSendScreenEntryTag.amount
                        }
                        return state
                    }
                    if let focusItemTag = focusItemTag {
                        arguments.selectNextInputItem(focusItemTag)
                    } else {
                        arguments.dismissInput()
                    }
                } else if isValidAddress(text) {
                    arguments.updateText(WalletSendScreenEntryTag.address, text)
                    if isValidAddress(text, exactLength: true) {
                        arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
                    }
                }
            }, tag: WalletSendScreenEntryTag.address, action: {
                arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
            }, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: {
                arguments.openQrScanner()
            }))
        case let .addressInfo(theme, text):
            return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
        case let .amountHeader(theme, text, balance, insufficient):
            return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section)
        case let .amount(theme, strings, placeholder, text):
            return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, clearType: .onFocus, tag: WalletSendScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in
                let text = formatAmountText(text, decimalSeparator: arguments.context.presentationData.dateTimeFormat.decimalSeparator)
                arguments.updateText(WalletSendScreenEntryTag.amount, text)
            }, shouldUpdateText: { text in
                return isValidAmount(text)
            }, processPaste: { pastedText in
                if isValidAmount(pastedText) {
                    let presentationData = arguments.context.presentationData
                    return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
                } else {
                    return text
                }
            }, updatedFocus: { focus in
                if !focus {
                    let presentationData = arguments.context.presentationData
                    arguments.updateState { state in
                        var state = state
                        if !state.amount.isEmpty {
                            state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
                        }
                        return state
                    }
                }
            }, action: {
                arguments.selectNextInputItem(WalletSendScreenEntryTag.amount)
            })
        case let .commentHeader(theme, text):
            return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
        case let .comment(theme, placeholder, value):
            return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: walletTextLimit, display: true, mode: .bytes), sectionId: self.section, style: .blocks, returnKeyType: .send, textUpdated: { text in
                arguments.updateText(WalletSendScreenEntryTag.comment, text)
            }, updatedFocus: { focus in
                if focus {
                    arguments.scrollToBottom()
                }
            }, tag: WalletSendScreenEntryTag.comment, action: {
                arguments.proceed()
            })
        }
    }
}

private struct WalletSendScreenState: Equatable {
    var address: String
    var amount: String
    var comment: String
}

private func walletSendScreenEntries(presentationData: WalletPresentationData, balance: Int64?, state: WalletSendScreenState) -> [WalletSendScreenEntry] {
    if balance == nil {
        return []
    }
    var entries: [WalletSendScreenEntry] = []

    entries.append(.addressHeader(presentationData.theme, presentationData.strings.Wallet_Send_AddressHeader))
    entries.append(.address(presentationData.theme, presentationData.strings.Wallet_Send_AddressText, state.address))
    entries.append(.addressInfo(presentationData.theme, presentationData.strings.Wallet_Send_AddressInfo))
    
    let amount = amountValue(state.amount)
    let balance = max(0, balance ?? 0)
    entries.append(.amountHeader(presentationData.theme, presentationData.strings.Wallet_Receive_AmountHeader, presentationData.strings.Wallet_Send_Balance(formatBalanceText(balance, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)).0, balance == 0 || (amount > 0 && balance < amount)))
    entries.append(.amount(presentationData.theme, presentationData.strings, presentationData.strings.Wallet_Send_AmountText, state.amount ?? ""))
    
    entries.append(.commentHeader(presentationData.theme, presentationData.strings.Wallet_Receive_CommentHeader))
    entries.append(.comment(presentationData.theme, presentationData.strings.Wallet_Receive_CommentInfo, state.comment))
    return entries
}

protocol WalletSendScreen {
    
}

private final class WalletSendScreenImpl: ItemListController, WalletSendScreen {
    
}

public func walletSendScreen(context: WalletContext, randomId: Int64, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil, comment: String? = nil) -> ViewController {    
    let presentationData = context.presentationData
   
    let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: comment ?? "")
    let statePromise = ValuePromise(initialState, ignoreRepeated: true)
    let stateValue = Atomic(value: initialState)
    let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in
        statePromise.set(stateValue.modify { f($0) })
    }
    
    let serverSaltValue = Promise<Data?>()
    serverSaltValue.set(context.getServerSalt()
    |> map(Optional.init)
    |> `catch` { _ -> Signal<Data?, NoError> in
        return .single(nil)
    })
    
    var presentControllerImpl: ((ViewController, Any?) -> Void)?
    var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)?
    var pushImpl: ((ViewController) -> Void)?
    var popImpl: (() -> Void)?
    var dismissImpl: (() -> Void)?
    var dismissInputImpl: (() -> Void)?
    var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)?
    var ensureItemVisibleImpl: ((WalletSendScreenEntryTag, Bool) -> Void)?
    
    let arguments = WalletSendScreenArguments(context: context, updateState: { f in
        updateState(f)
    }, updateText: { tag, value in
        updateState { state in
            var state = state
            switch tag {
            case .address:
                state.address = value
            case .amount:
                state.amount = value
            case .comment:
                state.comment = value
            }
            return state
        }
        ensureItemVisibleImpl?(tag, false)
    }, selectNextInputItem: { tag in
        selectNextInputItemImpl?(tag)
    }, scrollToBottom: {
        ensureItemVisibleImpl?(WalletSendScreenEntryTag.comment, true)
    }, dismissInput: {
        dismissInputImpl?()
    }, openQrScanner: {
        dismissInputImpl?()
        
        context.authorizeAccessToCamera(completion: {
            pushImpl?(WalletQrScanScreen(context: context, completion: { parsedUrl in
                var updatedState: WalletSendScreenState?
                updateState { state in
                    var state = state
                    state.address = parsedUrl.address
                    if let amount = parsedUrl.amount {
                        state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
                    }
                    if let comment = parsedUrl.comment {
                        state.comment = comment
                    }
                    updatedState = state
                    return state
                }
                popImpl?()
                if let updatedState = updatedState {
                    if updatedState.amount.isEmpty {
                        selectNextInputItemImpl?(WalletSendScreenEntryTag.address)
                    } else if updatedState.comment.isEmpty {
                        selectNextInputItemImpl?(WalletSendScreenEntryTag.amount)
                    }
                }
            }))
        })
    }, proceed: {
        let presentationData = context.presentationData
        let state = stateValue.with { $0 }
        let amount = amountValue(state.amount)
        
        updateState { state in
            var state = state
            state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
            return state
        }
        
        let title = NSAttributedString(string: presentationData.strings.Wallet_Send_Confirmation, font: Font.semibold(17.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
        
        let address = state.address[state.address.startIndex..<state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)] + " \n " + state.address[state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)..<state.address.endIndex]
        
        let text = presentationData.strings.Wallet_Send_ConfirmationText(formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), String(address)).0
        let bodyAttributes = MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
        let boldAttributes = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.list.itemPrimaryTextColor)
        let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil }), textAlignment: .center))
        attributedText.addAttribute(.font, value: Font.monospace(14.0), range: NSMakeRange(attributedText.string.count - address.count - 1, address.count))
        
        var dismissAlertImpl: ((Bool) -> Void)?
        let theme = context.presentationData.theme
        let controller = richTextAlertController(alertContext: AlertControllerContext(theme: theme.alert, themeSignal: .single(theme.alert)), title: title, text: attributedText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Wallet_Navigation_Cancel, action: {
            dismissAlertImpl?(true)
        }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Wallet_Send_ConfirmationConfirm, action: {
            dismissAlertImpl?(false)
            dismissInputImpl?()
            
            let presentationData = context.presentationData
            let progressSignal = Signal<Never, NoError> { subscriber in
                let controller = OverlayStatusController(theme: presentationData.theme,  type: .loading(cancelled: nil))
                presentControllerImpl?(controller, nil)
                return ActionDisposable { [weak controller] in
                    Queue.mainQueue().async() {
                        controller?.dismiss()
                    }
                }
            }
            |> runOn(Queue.mainQueue())
            |> delay(0.15, queue: Queue.mainQueue())
            let progressDisposable = progressSignal.start()
            
            var serverSaltSignal = serverSaltValue.get()
            |> take(1)
            
            serverSaltSignal = serverSaltSignal
            |> afterDisposed {
                Queue.mainQueue().async {
                    progressDisposable.dispose()
                }
            }
            
            let _ = (serverSaltSignal
            |> deliverOnMainQueue).start(next: { serverSalt in
                if let serverSalt = serverSalt {
                    if let commentData = state.comment.data(using: .utf8) {
                        pushImpl?(WalletSplashScreen(context: context, mode: .sending(walletInfo, state.address, amount, commentData, randomId, serverSalt), walletCreatedPreloadState: nil))
                    }
                }
            })
        })], allowInputInset: false, dismissAutomatically: false)
        presentInGlobalOverlayImpl?(controller, nil)
        
        dismissAlertImpl = { [weak controller] animated in
            if animated {
                controller?.dismissAnimated()
            } else {
                controller?.dismiss()
            }
        }
    })
    
    let walletState: Signal<WalletState?, NoError> = getCombinedWalletState(storage: context.storage, subject: .wallet(walletInfo), tonInstance: context.tonInstance, onlyCached: true)
    |> map { combinedState -> WalletState? in
        var state: WalletState?
        switch combinedState {
        case let .cached(combinedState):
            state = combinedState?.walletState
        case let .updated(combinedState):
            state = combinedState.walletState
        }
        return state
    }
    |> `catch` { _ -> Signal<WalletState?, NoError> in
        return .single(nil)
        |> then(
            getCombinedWalletState(storage: context.storage, subject: .wallet(walletInfo), tonInstance: context.tonInstance, onlyCached: false)
            |> map { combinedState -> WalletState? in
                var state: WalletState?
                switch combinedState {
                case let .cached(combinedState):
                    state = combinedState?.walletState
                case let .updated(combinedState):
                    state = combinedState.walletState
                }
                return state
            }
            |> `catch` { _ -> Signal<WalletState?, NoError> in
                return .single(nil)
            }
        )
    }

    var focusItemTag: ItemListItemTag?
    if address == nil {
        focusItemTag = WalletSendScreenEntryTag.address
    } else if amount == nil {
        focusItemTag = WalletSendScreenEntryTag.amount
    }
    
    let signal = combineLatest(queue: .mainQueue(), .single(context.presentationData), walletState, statePromise.get())
    |> map { presentationData, walletState, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
        let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Navigation_Cancel), style: .regular, enabled: true, action: {
            dismissImpl?()
        })
        
        let rightNavigationButton: ItemListNavigationButton?
        
        let amount = amountValue(state.amount)
        var sendEnabled = false
        var emptyItem: ItemListControllerEmptyStateItem?
        if let walletState = walletState {
            let textLength: Int = state.comment.data(using: .utf8, allowLossyConversion: true)?.count ?? 0
            sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= walletState.balance && textLength <= walletTextLimit

            rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Wallet_Send_Send), style: .bold, enabled: sendEnabled, action: {
                arguments.proceed()
            })
        } else {
            rightNavigationButton = nil
            emptyItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
        }

        let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Wallet_Send_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Wallet_Navigation_Back), animateChanges: false)
        let listState = ItemListNodeState(entries: walletSendScreenEntries(presentationData: presentationData, balance: walletState?.balance, state: state), style: .blocks, focusItemTag: focusItemTag, emptyStateItem: emptyItem, animateChanges: false)
        
        return (controllerState, (listState, arguments))
    }
    
    let controller = WalletSendScreenImpl(theme: context.presentationData.theme, strings: context.presentationData.strings, updatedPresentationData: .single((context.presentationData.theme, context.presentationData.strings)), state: signal, tabBarItem: nil)
    controller.navigationPresentation = .modal
    controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
    presentControllerImpl = { [weak controller] c, a in
        controller?.present(c, in: .window(.root), with: a)
    }
    presentInGlobalOverlayImpl = { [weak controller] c, a in
        controller?.presentInGlobalOverlay(c, with: a)
    }
    pushImpl = { [weak controller] c in
        controller?.push(c)
    }
    popImpl = { [weak controller] in
        (controller?.navigationController as? NavigationController)?.popViewController(animated: true)
    }
    dismissImpl = { [weak controller] in
        controller?.view.endEditing(true)
        let _ = controller?.dismiss()
    }
    dismissInputImpl = { [weak controller] in
        controller?.view.endEditing(true)
    }
    selectNextInputItemImpl = { [weak controller] currentTag in
        guard let controller = controller else {
            return
        }
        var resultItemNode: ItemListItemFocusableNode?
        var focusOnNext = false
        let _ = controller.frameForItemNode({ itemNode in
            if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, let focusableItemNode = itemNode as? ItemListItemFocusableNode {
                if focusOnNext && resultItemNode == nil {
                    resultItemNode = focusableItemNode
                    return true
                } else if currentTag.isEqual(to: tag) {
                    focusOnNext = true
                }
            }
            return false
        })
        if let resultItemNode = resultItemNode {
            resultItemNode.focus()
        }
    }
    ensureItemVisibleImpl = { [weak controller] targetTag, animated in
        controller?.afterLayout({
            guard let controller = controller else {
                return
            }
            
            var resultItemNode: ListViewItemNode?
            let state = stateValue.with({ $0 })
            let _ = controller.frameForItemNode({ itemNode in
                if let itemNode = itemNode as? ItemListItemNode {
                    if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
                        resultItemNode = itemNode as? ListViewItemNode
                        return true
                    }
                }
                return false
            })
    
            if let resultItemNode = resultItemNode {
                controller.ensureItemNodeVisible(resultItemNode, animated: animated)
            }
        })
    }
    return controller
}