//
//  AttachmentRowController.swift
//  ComposeUI
//
//  Created by Shadowfacts on 3/12/23.
//

import SwiftUI
import TuskerComponents
import Vision
import MatchedGeometryPresentation

class AttachmentRowController: ViewController {
    let parent: ComposeController
    let attachment: DraftAttachment
    
    @Published var descriptionMode: DescriptionMode = .allowEntry
    @Published var textRecognitionError: Error?
    @Published var focusAttachmentOnTextEditorUnfocus = false
    
    let thumbnailController: AttachmentThumbnailController
    
    private var descriptionObservation: NSKeyValueObservation?
    
    init(parent: ComposeController, attachment: DraftAttachment) {
        self.parent = parent
        self.attachment = attachment
        self.thumbnailController = AttachmentThumbnailController(attachment: attachment, parent: parent)
        
        descriptionObservation = attachment.observe(\.attachmentDescription, changeHandler: { [unowned self] _, _ in
            // the faultingState is non-zero for objects that are being cascade deleted when the draft is deleted
            if attachment.faultingState == 0 {
                self.updateAttachmentDescriptionState()
            }
        })
    }
    
    private func updateAttachmentDescriptionState() {
        if attachment.attachmentDescription.isEmpty {
            parent.attachmentsMissingDescriptions.insert(attachment.id)
        } else {
            parent.attachmentsMissingDescriptions.remove(attachment.id)
        }
    }
    
    var view: some View {
        AttachmentView(attachment: attachment)
    }
    
    private func removeAttachment() {
        withAnimation {
            var newAttachments = parent.draft.draftAttachments
            newAttachments.removeAll(where: { $0.id == attachment.id })
            parent.draft.attachments = NSMutableOrderedSet(array: newAttachments)
        }
    }
    
    private func editDrawing() {
        guard case .drawing(let drawing) = attachment.data else {
            return
        }
        parent.config.presentDrawing?(drawing) { newDrawing in
            self.attachment.drawing = newDrawing
        }
    }
    
    private func focusAttachment() {
        focusAttachmentOnTextEditorUnfocus = false
        parent.focusedAttachment = (attachment, thumbnailController)
    }
    
    private func recognizeText() {
        descriptionMode = .recognizingText
        
        self.attachment.getData(features: self.parent.mastodonController.instanceFeatures, skipAllConversion: true) { result in
            DispatchQueue.main.async {
                let data: Data
                switch result {
                case .success((let d, _)):
                    data = d
                case .failure(let error):
                    self.descriptionMode = .allowEntry
                    self.textRecognitionError = error
                    return
                }
                
                let handler = VNImageRequestHandler(data: data)
                let request = VNRecognizeTextRequest { request, error in
                    DispatchQueue.main.async {
                        if let results = request.results as? [VNRecognizedTextObservation] {
                            var text = ""
                            for observation in results {
                                let result = observation.topCandidates(1).first!
                                text.append(result.string)
                                text.append("\n")
                            }
                            self.attachment.attachmentDescription = text
                        }
                        self.descriptionMode = .allowEntry
                    }
                }
                request.recognitionLevel = .accurate
                request.usesLanguageCorrection = true
                DispatchQueue.global(qos: .userInitiated).async {
                    do {
                        try handler.perform([request])
                    } catch let error as NSError where error.code == 1 {
                        // The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
                        return
                    } catch {
                        DispatchQueue.main.async {
                            self.descriptionMode = .allowEntry
                            self.textRecognitionError = error
                        }
                    }
                }
            }
        }
    }
    
    struct AttachmentView: View {
        @ObservedObject private var attachment: DraftAttachment
        @EnvironmentObject private var controller: AttachmentRowController
        @FocusState private var textEditorFocused: Bool
        
        init(attachment: DraftAttachment) {
            self.attachment = attachment
        }
        
        var body: some View {
            HStack(alignment: .center, spacing: 4) {
                ControllerView(controller: { controller.thumbnailController })
                    .clipShape(RoundedRectangle(cornerRadius: 8))
                    .environment(\.attachmentThumbnailConfiguration, .init(contentMode: .fit, fullSize: false))
                    .matchedGeometrySource(id: attachment.id, presentationID: attachment.id)
                    .overlay {
                        thumbnailFocusedOverlay
                    }
                    .frame(width: 80, height: 80)
                    .onTapGesture {
                        textEditorFocused = false
                        // if we just focus the attachment immediately, the text editor doesn't actually unfocus
                        controller.focusAttachmentOnTextEditorUnfocus = true
                    }
                    .contextMenu {
                        if attachment.drawingData != nil {
                            Button(action: controller.editDrawing) {
                                Label("Edit Drawing", systemImage: "hand.draw")
                            }
                        } else if attachment.type == .image {
                            Button(action: controller.recognizeText) {
                                Label("Recognize Text", systemImage: "doc.text.viewfinder")
                            }
                        }

                        Button(role: .destructive, action: controller.removeAttachment) {
                            Label("Delete", systemImage: "trash")
                        }
                    } previewIfAvailable: {
                        ControllerView(controller: { controller.thumbnailController })
                    }
                
                switch controller.descriptionMode {
                case .allowEntry:
                    InlineAttachmentDescriptionView(attachment: attachment, minHeight: 80)
                        .matchedGeometrySource(id: AttachmentDescriptionTextViewID(attachment), presentationID: attachment.id)
                        .focused($textEditorFocused)
                    
                case .recognizingText:
                    ProgressView()
                        .progressViewStyle(.circular)
                }
            }
            .alertWithData("Text Recognition Failed", data: $controller.textRecognitionError) { _ in
                Button("OK") {}
            } message: { error in
                Text(error.localizedDescription)
            }
            .onAppear(perform: controller.updateAttachmentDescriptionState)
            .onChange(of: textEditorFocused) { newValue in
                if !newValue && controller.focusAttachmentOnTextEditorUnfocus {
                    controller.focusAttachment()
                }
            }
        }
        
        @ViewBuilder
        private var thumbnailFocusedOverlay: some View {
            Image(systemName: "arrow.up.backward.and.arrow.down.forward")
                .foregroundColor(.white)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.black.opacity(0.35))
                .clipShape(RoundedRectangle(cornerRadius: 8))
                // use .opacity and an animation, because .transition doesn't seem to play nice with @FocusState
                .opacity(textEditorFocused ? 1 : 0)
                .animation(.linear(duration: 0.1), value: textEditorFocused)
        }

    }
    
}

extension AttachmentRowController {
    enum DescriptionMode {
        case allowEntry, recognizingText
    }
}

private extension View {
    @available(iOS, obsoleted: 16.0)
    @ViewBuilder
    func contextMenu<M: View, P: View>(@ViewBuilder menuItems: () -> M, @ViewBuilder previewIfAvailable preview: () -> P) -> some View {
        if #available(iOS 16.0, *) {
            self.contextMenu(menuItems: menuItems, preview: preview)
        } else {
            self.contextMenu(menuItems: menuItems)
        }
    }
}
