import SwiftUI
import AppKit

// MARK: - App Grid Shared Support

enum AppGridItemMetrics {
    static let hoverScale: CGFloat = 1.22
    static let labelHeight: CGFloat = 14

    static func stableWidth(iconSize: CGFloat) -> CGFloat {
        iconSize * hoverScale + 8
    }

    static func stableHeight(iconSize: CGFloat) -> CGFloat {
        iconSize * hoverScale + labelHeight + 22
    }
}

enum AppBubbleHoverEvent {
    case entered(canShowBubble: Bool)
    case exited
}

enum BubblePlacement {
    case above
    case below
}

struct AppNameBubble: View {
    let appName: String
    let note: String?
    let isEditing: Bool
    let placement: BubblePlacement
    let arrowOffset: CGFloat
    let noteQuotaText: String?
    let noteQuotaWarning: Bool
    @Binding var draftNote: String
    var noteFocused: FocusState<Bool>.Binding
    let onCommit: () -> Void
    let onCancel: () -> Void

    private var displayNote: String {
        note?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    }

    var body: some View {
        content
        .onExitCommand(perform: onCancel)
    }

    private var content: some View {
        VStack(spacing: isEditing || !displayNote.isEmpty ? 8 : 0) {
            Text(appName)
                .font(.system(size: isEditing ? 22 : 24, weight: .bold))
                .foregroundStyle(.white)
                .lineLimit(1)
                .truncationMode(.middle)
                .frame(maxWidth: .infinity)

            if isEditing {
                VStack(alignment: .trailing, spacing: 4) {
                    AppBubbleNoteTextField(
                        text: $draftNote,
                        placeholder: tr("appNote.placeholder"),
                        isFocused: noteFocused,
                        onCommit: onCommit,
                        onCancel: onCancel
                    )
                        .padding(.horizontal, 12)
                        .frame(height: 36)
                        .background(
                            RoundedRectangle(cornerRadius: 9)
                                .fill(Color.white.opacity(0.12))
                        )
                    Text("\(min(draftNote.count, TagDatabase.maxAppNoteLength)) / \(TagDatabase.maxAppNoteLength)")
                        .font(.system(size: 10, weight: .medium))
                        .foregroundStyle(.white.opacity(0.35))

                    if let noteQuotaText, !noteQuotaText.isEmpty {
                        Text(noteQuotaText)
                            .font(.system(size: 11, weight: .medium))
                            .foregroundStyle(
                                noteQuotaWarning
                                    ? Color.orange.opacity(0.96)
                                    : Color.white.opacity(0.58)
                            )
                            .multilineTextAlignment(.trailing)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
            } else if !displayNote.isEmpty {
                Text(displayNote)
                    .font(.system(size: 14, weight: .medium))
                    .lineSpacing(2)
                    .foregroundStyle(.white.opacity(0.78))
                    .lineLimit(4)
                    .multilineTextAlignment(.center)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity)
            }
        }
        .padding(.horizontal, isEditing ? 22 : 24)
        .padding(.vertical, isEditing ? 18 : 16)
        .background(
            RoundedRectangle(cornerRadius: 18, style: .continuous)
                .fill(Color.black.opacity(0.92))
                .shadow(color: .black.opacity(0.34), radius: 24, y: 16)
                .shadow(color: .black.opacity(0.18), radius: 8, y: 3)
        )
        .overlay(alignment: placement == .above ? .bottom : .top) {
            BubbleArrow(placement: placement)
                .fill(Color.black.opacity(0.92))
                .frame(width: 18, height: 9)
                .offset(x: arrowOffset, y: placement == .above ? 8 : -8)
        }
    }
}

private struct AppBubbleNoteTextField: NSViewRepresentable {
    @Binding var text: String
    let placeholder: String
    var isFocused: FocusState<Bool>.Binding
    let onCommit: () -> Void
    let onCancel: () -> Void
    private static let placeholderAttributes: [NSAttributedString.Key: Any] = [
        .foregroundColor: NSColor.white.withAlphaComponent(0.48)
    ]

    func makeCoordinator() -> Coordinator {
        Coordinator(
            text: $text,
            isFocused: isFocused,
            onCommit: onCommit,
            onCancel: onCancel
        )
    }

    func makeNSView(context: Context) -> AppBubbleNativeTextField {
        let field = AppBubbleNativeTextField()
        field.isBordered = false
        field.isBezeled = false
        field.drawsBackground = false
        field.focusRingType = .none
        field.font = NSFont.systemFont(ofSize: 15, weight: .medium)
        field.textColor = .white
        field.placeholderAttributedString = NSAttributedString(
            string: placeholder,
            attributes: Self.placeholderAttributes
        )
        field.delegate = context.coordinator
        field.target = context.coordinator
        field.action = #selector(Coordinator.submitAction(_:))
        field.cell?.usesSingleLineMode = true
        field.cell?.wraps = false
        field.lineBreakMode = .byTruncatingTail
        field.setAccessibilityLabel(placeholder)
        context.coordinator.field = field
        return field
    }

    func updateNSView(_ field: AppBubbleNativeTextField, context: Context) {
        context.coordinator.text = $text
        context.coordinator.isFocused = isFocused
        context.coordinator.onCommit = onCommit
        context.coordinator.onCancel = onCancel
        context.coordinator.field = field
        field.placeholderAttributedString = NSAttributedString(
            string: placeholder,
            attributes: Self.placeholderAttributes
        )
        field.setAccessibilityLabel(placeholder)

        if !field.hasMarkedTextInput, field.stringValue != text {
            field.stringValue = text
        }

        guard isFocused.wrappedValue, field.window != nil, field.currentEditor() == nil else { return }
        DispatchQueue.main.async { [weak field] in
            guard let field, field.window != nil, field.currentEditor() == nil else { return }
            field.window?.makeFirstResponder(field)
        }
    }

    final class Coordinator: NSObject, NSTextFieldDelegate {
        var text: Binding<String>
        var isFocused: FocusState<Bool>.Binding
        var onCommit: () -> Void
        var onCancel: () -> Void
        weak var field: AppBubbleNativeTextField?

        init(
            text: Binding<String>,
            isFocused: FocusState<Bool>.Binding,
            onCommit: @escaping () -> Void,
            onCancel: @escaping () -> Void
        ) {
            self.text = text
            self.isFocused = isFocused
            self.onCommit = onCommit
            self.onCancel = onCancel
        }

        func controlTextDidChange(_ notification: Notification) {
            guard let field = notification.object as? AppBubbleNativeTextField,
                  !field.hasMarkedTextInput
            else { return }
            syncFinalText(from: field)
        }

        func controlTextDidEndEditing(_ notification: Notification) {
            if let field = notification.object as? AppBubbleNativeTextField {
                syncFinalText(from: field)
            }
            isFocused.wrappedValue = false
        }

        func control(
            _ control: NSControl,
            textView: NSTextView,
            doCommandBy commandSelector: Selector
        ) -> Bool {
            if textView.hasMarkedText() {
                return false
            }

            switch commandSelector {
            case #selector(NSResponder.insertNewline(_:)):
                if let field = control as? AppBubbleNativeTextField {
                    syncFinalText(from: field)
                }
                onCommit()
                return true
            case #selector(NSResponder.cancelOperation(_:)):
                isFocused.wrappedValue = false
                onCancel()
                return true
            default:
                return false
            }
        }

        @objc func submitAction(_ sender: Any?) {
            guard let field = sender as? AppBubbleNativeTextField,
                  !field.hasMarkedTextInput
            else { return }
            syncFinalText(from: field)
            onCommit()
        }

        private func syncFinalText(from field: AppBubbleNativeTextField) {
            guard !field.hasMarkedTextInput else { return }
            let limited = String(field.stringValue.prefix(TagDatabase.maxAppNoteLength))
            if field.stringValue != limited {
                field.stringValue = limited
            }
            text.wrappedValue = limited
        }
    }
}

private final class AppBubbleNativeTextField: NSTextField {
    var hasMarkedTextInput: Bool {
        (currentEditor() as? NSTextView)?.hasMarkedText() == true
    }
}

private struct BubbleArrow: Shape {
    let placement: BubblePlacement

    func path(in rect: CGRect) -> Path {
        var path = Path()
        switch placement {
        case .above:
            path.move(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
        case .below:
            path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
        }
        path.closeSubpath()
        return path
    }
}
