//
//  MainTextView.swift
//  ComposeUI
//
//  Created by Shadowfacts on 3/6/23.
//

import SwiftUI

struct MainTextView: View {
    @EnvironmentObject private var controller: ComposeController
    @EnvironmentObject private var draft: Draft
    @Environment(\.colorScheme) private var colorScheme
    @ScaledMetric private var fontSize = 20
    
    @State private var hasFirstAppeared = false
    @State private var height: CGFloat?
    @State private var updateSelection: ((UITextView) -> Void)?
    private let minHeight: CGFloat = 150
    private var effectiveHeight: CGFloat { height ?? minHeight }
    
    var config: ComposeUIConfig {
        controller.config
    }
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            colorScheme == .dark ? config.fillColor : Color(uiColor: .secondarySystemBackground)
            
            if draft.text.isEmpty {
                ControllerView(controller: { PlaceholderController() })
                    .font(.system(size: fontSize))
                    .foregroundColor(.secondary)
                    .offset(x: 4, y: 8)
                    .accessibilityHidden(true)
            }
            
            MainWrappedTextViewRepresentable(text: $draft.text, becomeFirstResponder: $controller.mainComposeTextViewBecomeFirstResponder, updateSelection: $updateSelection, textDidChange: textDidChange)
        }
        .frame(height: effectiveHeight)
        .onAppear(perform: becomeFirstResponderOnFirstAppearance)
    }
    
    private func becomeFirstResponderOnFirstAppearance() {
        if !hasFirstAppeared {
            hasFirstAppeared = true
            controller.mainComposeTextViewBecomeFirstResponder = true
            if config.textSelectionStartsAtBeginning {
                updateSelection = { textView in
                    textView.selectedTextRange = textView.textRange(from: textView.beginningOfDocument, to: textView.beginningOfDocument)
                }
            }
        }
    }
    
    private func textDidChange(textView: UITextView) {
        height = max(textView.contentSize.height, minHeight)
    }
}

fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
    typealias UIViewType = UITextView
    
    @Binding var text: String
    @Binding var becomeFirstResponder: Bool
    @Binding var updateSelection: ((UITextView) -> Void)?
    let textDidChange: (UITextView) -> Void
    
    @EnvironmentObject private var controller: ComposeController
    @Environment(\.isEnabled) private var isEnabled: Bool
    
    func makeUIView(context: Context) -> UITextView {
        let textView = WrappedTextView(composeController: controller)
        context.coordinator.textView = textView
        textView.delegate = context.coordinator
        textView.isEditable = true
        textView.backgroundColor = .clear
        textView.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 20))
        textView.adjustsFontForContentSizeCategory = true
        textView.textContainer.lineBreakMode = .byWordWrapping
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        if text != uiView.text {
            context.coordinator.skipNextSelectionChangedAutocompleteUpdate = true
            uiView.text = text
        }
        
        uiView.isEditable = isEnabled
        uiView.keyboardType = controller.config.useTwitterKeyboard ? .twitter : .default
        
        context.coordinator.text = $text
        
        if let updateSelection {
            updateSelection(uiView)
            self.updateSelection = nil
        }
        
        // wait until the next runloop iteration so that SwiftUI view updates have finished and
        // the text view knows its new content size
        DispatchQueue.main.async {
            textDidChange(uiView)
            
            if becomeFirstResponder {
                // calling becomeFirstResponder during the SwiftUI update causes a crash on iOS 13
                uiView.becomeFirstResponder()
                // can't update @State vars during the SwiftUI update
                becomeFirstResponder = false
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(controller: controller, text: $text, textDidChange: textDidChange)
    }
    
    class WrappedTextView: UITextView {
        private let formattingActions = [#selector(toggleBoldface(_:)), #selector(toggleItalics(_:))]
        private let composeController: ComposeController
        
        init(composeController: ComposeController) {
            self.composeController = composeController
            super.init(frame: .zero, textContainer: nil)
        }
        
        required init?(coder: NSCoder) {
            fatalError()
        }
        
        override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
            if formattingActions.contains(action) {
                return composeController.config.contentType != .plain
            }
            return super.canPerformAction(action, withSender: sender)
        }
        
        override func toggleBoldface(_ sender: Any?) {
            (delegate as! Coordinator).applyFormat(.bold)
        }
        
        override func toggleItalics(_ sender: Any?) {
            (delegate as! Coordinator).applyFormat(.italics)
        }
        
        override func validate(_ command: UICommand) {
            super.validate(command)
            
            if formattingActions.contains(command.action),
               composeController.config.contentType != .plain {
                command.attributes.remove(.disabled)
            }
        }
        
        override func paste(_ sender: Any?) {
            // we deliberately exclude the other CompositionAttachment readable type identifiers, because that's too overzealous with the conversion
            // and things like URLs end up pasting as attachments
            if UIPasteboard.general.contains(pasteboardTypes: UIImage.readableTypeIdentifiersForItemProvider) {
                composeController.paste(itemProviders: UIPasteboard.general.itemProviders)
            } else {
                super.paste(sender)
            }
        }
    }
    
    class Coordinator: NSObject, UITextViewDelegate, ComposeInput, TextViewCaretScrolling {
        weak var textView: UITextView?
        
        let controller: ComposeController
        var text: Binding<String>
        let textDidChange: (UITextView) -> Void
        
        var caretScrollPositionAnimator: UIViewPropertyAnimator?
        
        @Published var autocompleteState: AutocompleteState?
        var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { $autocompleteState }
        var skipNextSelectionChangedAutocompleteUpdate = false
        
        init(controller: ComposeController, text: Binding<String>, textDidChange: @escaping (UITextView) -> Void) {
            self.controller = controller
            self.text = text
            self.textDidChange = textDidChange
            
            super.init()
            
            NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
        }
        
        @objc private func keyboardDidShow() {
            guard let textView,
                  textView.isFirstResponder else {
                return
            }
            ensureCursorVisible(textView: textView)
        }
        
        // MARK: UITextViewDelegate
        
        func textViewDidChange(_ textView: UITextView) {
            text.wrappedValue = textView.text
            textDidChange(textView)
            
            ensureCursorVisible(textView: textView)
        }
        
        func textViewDidBeginEditing(_ textView: UITextView) {
            controller.currentInput = self
            updateAutocompleteState()
        }
        
        func textViewDidEndEditing(_ textView: UITextView) {
            controller.currentInput = nil
            updateAutocompleteState()
        }
        
        func textViewDidChangeSelection(_ textView: UITextView) {
            if skipNextSelectionChangedAutocompleteUpdate {
                skipNextSelectionChangedAutocompleteUpdate = false
            } else {
                updateAutocompleteState()
            }
        }
        
        func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
            var actions = suggestedActions
            if controller.config.contentType != .plain,
               let index = suggestedActions.firstIndex(where: { ($0 as? UIMenu)?.identifier.rawValue == "com.apple.menu.format" }) {
                if range.length > 0 {
                    let formatMenu = suggestedActions[index] as! UIMenu
                    let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
                        var image: UIImage?
                        if let imageName = fmt.imageName {
                            image = UIImage(systemName: imageName)
                        }
                        return UIAction(title: fmt.accessibilityLabel, image: image) { [weak self] _ in
                            self?.applyFormat(fmt)
                        }
                    })
                    actions[index] = newFormatMenu
                } else {
                    actions.remove(at: index)
                }
            }
            if range.length == 0 {
                actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in
                    self?.controller.shouldEmojiAutocompletionBeginExpanded = true
                    self?.beginAutocompletingEmoji()
                }))
            }
            return UIMenu(children: actions)
        }
        
        // MARK: ComposeInput
        
        var toolbarElements: [ToolbarElement] {
            [.emojiPicker, .formattingButtons]
        }
        
        var textInputMode: UITextInputMode? {
            textView?.textInputMode
        }
        
        func autocomplete(with string: String) {
            textView?.autocomplete(with: string, permittedModes: .all, autocompleteState: &autocompleteState)
        }
        
        func applyFormat(_ format: StatusFormat) {
            guard let textView,
                  textView.isFirstResponder,
                  let insertionResult = format.insertionResult(for: controller.config.contentType) else {
                return
            }
            
            let currentSelectedRange = textView.selectedRange
            if currentSelectedRange.length == 0 {
                textView.insertText(insertionResult.prefix + insertionResult.suffix)
                textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: 0)
            } else {
                let start = textView.text.utf16.index(textView.text.utf16.startIndex, offsetBy: currentSelectedRange.lowerBound)
                let end = textView.text.utf16.index(textView.text.utf16.startIndex, offsetBy: currentSelectedRange.upperBound)
                let selectedText = textView.text.utf16[start..<end]
                textView.insertText(insertionResult.prefix + String(Substring(selectedText)) + insertionResult.suffix)
                textView.selectedRange = NSRange(location: currentSelectedRange.location + insertionResult.insertionPoint, length: currentSelectedRange.length)
            }
        }
        
        func beginAutocompletingEmoji() {
            guard let textView else {
                return
            }
            var insertSpace = false
            if let text = textView.text,
               textView.selectedRange.upperBound > 0 {
                let characterBeforeCursorIndex = text.utf16.index(before: text.utf16.index(text.startIndex, offsetBy: textView.selectedRange.upperBound))
                insertSpace = !text[characterBeforeCursorIndex].isWhitespace
            }
            textView.insertText((insertSpace ? " " : "") + ":")
        }
        
        private func updateAutocompleteState() {
            guard let textView else {
                autocompleteState = nil
                return
            }
            autocompleteState = textView.updateAutocompleteState(permittedModes: .all)
        }
        
    }
}
