#if canImport(UIKit)
import UIKit
import VNCCore

@MainActor
final class FramebufferUIView: UIView,
                               UIGestureRecognizerDelegate,
                               UIPointerInteractionDelegate,
                               UIKeyInput {
    // MARK: - UITextInputTraits — disable every kind of autocomplete, so
    // each keystroke produces exactly one insertText() call and nothing
    // is swallowed by the suggestion/predictive engine.
    @objc var autocorrectionType: UITextAutocorrectionType = .no
    @objc var autocapitalizationType: UITextAutocapitalizationType = .none
    @objc var spellCheckingType: UITextSpellCheckingType = .no
    @objc var smartQuotesType: UITextSmartQuotesType = .no
    @objc var smartDashesType: UITextSmartDashesType = .no
    @objc var smartInsertDeleteType: UITextSmartInsertDeleteType = .no
    @objc var keyboardType: UIKeyboardType = .asciiCapable
    @objc var keyboardAppearance: UIKeyboardAppearance = .dark
    @objc var returnKeyType: UIReturnKeyType = .default
    @objc var enablesReturnKeyAutomatically: Bool = false
    @objc var isSecureTextEntry: Bool = false
    @objc var passwordRules: UITextInputPasswordRules?
    @objc var textContentType: UITextContentType? = UITextContentType(rawValue: "")

    weak var controller: SessionController?
    var inputMode: InputMode = .touch
    var selectedScreen: RemoteScreen? {
        didSet { setNeedsLayout() }
    }
    var zoomScale: CGFloat = 1.0 {
        didSet { setNeedsLayout() }
    }
    var contentTranslation: CGPoint = .zero {
        didSet { setNeedsLayout() }
    }
    var trackpadCursorNormalized: CGPoint = CGPoint(x: 0.5, y: 0.5)
    var onTrackpadCursorChanged: ((CGPoint) -> Void)?

    private let imageLayer = CALayer()
    private let inputMapper = InputMapper()

    // Touch-mode pan with implicit left-button drag
    private var touchPanActiveButtonDown = false
    private var touchPanLastNormalized: CGPoint?

    // Trackpad-mode pan moves the soft cursor
    private var trackpadPanStartCursor: CGPoint?
    private var trackpadPanStartPoint: CGPoint?

    // Two-finger scroll accumulator
    private var scrollAccumulator: CGPoint = .zero
    private static let scrollStepPoints: CGFloat = 28

    // Pinch zoom anchor
    private var pinchStartScale: CGFloat = 1.0
    private var pinchStartTranslation: CGPoint = .zero

    // Indirect pointer (trackpad/mouse via UIPointerInteraction)
    private var indirectPointerNormalized: CGPoint?

    // On-screen keyboard handling — direct UIKeyInput conformance.
    var onKeyboardDismissed: (() -> Void)?
    private lazy var functionAccessoryView: UIView = makeFunctionAccessoryView()
    private var keyboardWanted = false

    // A separate accessibility element for test diagnostics, because iOS
    // reserves `accessibilityValue` on UIKeyInput-classed views for text.
    private let diagnosticProbe: UIView = {
        let v = UIView(frame: .zero)
        v.alpha = 0
        v.isAccessibilityElement = true
        v.accessibilityIdentifier = "fb-diag"
        v.accessibilityLabel = ""
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        isOpaque = true
        backgroundColor = .black
        isMultipleTouchEnabled = true
        clipsToBounds = true

        imageLayer.magnificationFilter = .nearest
        imageLayer.minificationFilter = .linear
        imageLayer.contentsGravity = .resize
        // Kill CALayer's implicit animations — we want every framebuffer
        // update to be an instantaneous blit, not a 0.25s crossfade.
        imageLayer.actions = [
            "contents": NSNull(),
            "contentsRect": NSNull(),
            "position": NSNull(),
            "bounds": NSNull(),
            "frame": NSNull(),
            "transform": NSNull(),
            "backgroundColor": NSNull()
        ]
        layer.addSublayer(imageLayer)

        configureGestureRecognizers()
        configurePointerInteraction()

        isAccessibilityElement = true
        accessibilityLabel = "Remote framebuffer"
        accessibilityIdentifier = "framebuffer"

        addSubview(diagnosticProbe)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var canBecomeFirstResponder: Bool { true }

    override func didMoveToWindow() {
        super.didMoveToWindow()
        if window != nil { _ = becomeFirstResponder() }
    }

    override var inputAccessoryView: UIView? {
        keyboardWanted ? functionAccessoryView : nil
    }

    /// Show or hide the iOS on-screen keyboard.
    func setSoftwareKeyboardVisible(_ visible: Bool) {
        appendDiagnostic("set:\(visible)")
        if visible {
            keyboardWanted = true
            if !isFirstResponder {
                let became = becomeFirstResponder()
                appendDiagnostic("became:\(became)")
            } else {
                reloadInputViews()
                appendDiagnostic("reload")
            }
        } else {
            keyboardWanted = false
            if isFirstResponder {
                _ = resignFirstResponder()
                appendDiagnostic("resigned")
            }
        }
    }

    private func appendDiagnostic(_ tag: String) {
        let prior = diagnosticProbe.accessibilityLabel ?? ""
        diagnosticProbe.accessibilityLabel = prior + "[\(tag)]"
    }

    // MARK: - UIKeyInput

    var hasText: Bool { true }

    func insertText(_ text: String) {
        appendDiagnostic("ins:\(text)")
        controller?.type(text)
    }

    func deleteBackward() {
        appendDiagnostic("del")
        controller?.sendBackspace()
    }

    // MARK: Layout / image

    override func layoutSubviews() {
        super.layoutSubviews()
        applyLayerFrame()
    }

    func apply(image: CGImage?, framebufferSize: CGSize) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        imageLayer.contents = image
        applyLayerFrame()
        CATransaction.commit()
    }

    private func applyLayerFrame() {
        let bounds = self.bounds
        let fbSize = framebufferContentSize()
        guard fbSize.width > 0, fbSize.height > 0,
              bounds.width > 0, bounds.height > 0,
              let displayed = inputMapper.displayedRect(for: fbSize, in: bounds.size) else {
            imageLayer.frame = bounds
            imageLayer.contentsRect = CGRect(x: 0, y: 0, width: 1, height: 1)
            return
        }

        let scaled = displayed.applying(
            CGAffineTransform(translationX: -bounds.midX, y: -bounds.midY)
                .concatenating(CGAffineTransform(scaleX: zoomScale, y: zoomScale))
                .concatenating(CGAffineTransform(translationX: bounds.midX + contentTranslation.x,
                                                 y: bounds.midY + contentTranslation.y))
        )
        imageLayer.frame = scaled

        if let screen = selectedScreen {
            let totalWidth = framebufferAbsoluteSize().width
            let totalHeight = framebufferAbsoluteSize().height
            if totalWidth > 0, totalHeight > 0 {
                imageLayer.contentsRect = CGRect(
                    x: screen.frame.origin.x / totalWidth,
                    y: screen.frame.origin.y / totalHeight,
                    width: screen.frame.width / totalWidth,
                    height: screen.frame.height / totalHeight
                )
            } else {
                imageLayer.contentsRect = CGRect(x: 0, y: 0, width: 1, height: 1)
            }
        } else {
            imageLayer.contentsRect = CGRect(x: 0, y: 0, width: 1, height: 1)
        }
    }

    private func framebufferAbsoluteSize() -> CGSize {
        guard let size = controller?.framebufferSize else { return .zero }
        return CGSize(width: CGFloat(size.width), height: CGFloat(size.height))
    }

    private func framebufferContentSize() -> CGSize {
        if let screen = selectedScreen {
            return CGSize(width: screen.frame.width, height: screen.frame.height)
        }
        return framebufferAbsoluteSize()
    }

    // MARK: Gesture recognition

    private func configureGestureRecognizers() {
        let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleSingleTap(_:)))
        singleTap.numberOfTapsRequired = 1
        singleTap.numberOfTouchesRequired = 1
        addGestureRecognizer(singleTap)

        let twoFingerTap = UITapGestureRecognizer(target: self, action: #selector(handleTwoFingerTap(_:)))
        twoFingerTap.numberOfTapsRequired = 1
        twoFingerTap.numberOfTouchesRequired = 2
        addGestureRecognizer(twoFingerTap)

        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        longPress.minimumPressDuration = 0.55
        longPress.delegate = self
        addGestureRecognizer(longPress)

        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        pan.minimumNumberOfTouches = 1
        pan.maximumNumberOfTouches = 1
        pan.delegate = self
        pan.allowedTouchTypes = [
            NSNumber(value: UITouch.TouchType.direct.rawValue),
            NSNumber(value: UITouch.TouchType.pencil.rawValue)
        ]
        addGestureRecognizer(pan)

        let twoFingerPan = UIPanGestureRecognizer(target: self, action: #selector(handleTwoFingerPan(_:)))
        twoFingerPan.minimumNumberOfTouches = 2
        twoFingerPan.maximumNumberOfTouches = 2
        twoFingerPan.delegate = self
        addGestureRecognizer(twoFingerPan)

        let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
        pinch.delegate = self
        addGestureRecognizer(pinch)

        let indirectScroll = UIPanGestureRecognizer(target: self, action: #selector(handleIndirectScroll(_:)))
        indirectScroll.allowedScrollTypesMask = .all
        indirectScroll.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirectPointer.rawValue)]
        indirectScroll.delegate = self
        addGestureRecognizer(indirectScroll)

        let hover = UIHoverGestureRecognizer(target: self, action: #selector(handleHover(_:)))
        addGestureRecognizer(hover)
    }

    // MARK: UIGestureRecognizerDelegate

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
        if gestureRecognizer is UIPinchGestureRecognizer || other is UIPinchGestureRecognizer {
            return true
        }
        return false
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                           shouldRequireFailureOf other: UIGestureRecognizer) -> Bool {
        if let tap = gestureRecognizer as? UITapGestureRecognizer,
           tap.numberOfTouchesRequired == 1,
           let otherTap = other as? UITapGestureRecognizer,
           otherTap.numberOfTouchesRequired == 2 {
            return true
        }
        return false
    }

    // MARK: Pointer Interaction

    private func configurePointerInteraction() {
        let interaction = UIPointerInteraction(delegate: self)
        addInteraction(interaction)
    }

    func pointerInteraction(_ interaction: UIPointerInteraction,
                            styleFor region: UIPointerRegion) -> UIPointerStyle? {
        UIPointerStyle.hidden()
    }

    func pointerInteraction(_ interaction: UIPointerInteraction,
                            regionFor request: UIPointerRegionRequest,
                            defaultRegion: UIPointerRegion) -> UIPointerRegion? {
        let viewPoint = request.location
        if let normalized = inputMapper.normalize(viewPoint: viewPoint,
                                                  in: bounds.size,
                                                  framebufferSize: framebufferContentSize()) {
            indirectPointerNormalized = normalized
            controller?.pointerMove(toNormalized: mapToFullFramebuffer(normalized: normalized))
        }
        return defaultRegion
    }

    // MARK: Gesture Handlers

    @objc private func handleSingleTap(_ recognizer: UITapGestureRecognizer) {
        let location = recognizer.location(in: self)
        switch inputMode {
        case .touch:
            if let normalized = normalizedFor(location) {
                controller?.pointerClick(.left,
                                         atNormalized: mapToFullFramebuffer(normalized: normalized))
            }
        case .trackpad:
            controller?.pointerClick(.left,
                                     atNormalized: mapToFullFramebuffer(normalized: trackpadCursorNormalized))
        }
        if !isFirstResponder { _ = becomeFirstResponder() }
    }

    @objc private func handleTwoFingerTap(_ recognizer: UITapGestureRecognizer) {
        let location = recognizer.location(in: self)
        switch inputMode {
        case .touch:
            if let normalized = normalizedFor(location) {
                controller?.pointerClick(.right,
                                         atNormalized: mapToFullFramebuffer(normalized: normalized))
            }
        case .trackpad:
            controller?.pointerClick(.right,
                                     atNormalized: mapToFullFramebuffer(normalized: trackpadCursorNormalized))
        }
    }

    @objc private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
        guard recognizer.state == .began else { return }
        let location = recognizer.location(in: self)
        switch inputMode {
        case .touch:
            if let normalized = normalizedFor(location) {
                controller?.pointerClick(.right,
                                         atNormalized: mapToFullFramebuffer(normalized: normalized))
            }
        case .trackpad:
            controller?.pointerClick(.right,
                                     atNormalized: mapToFullFramebuffer(normalized: trackpadCursorNormalized))
        }
    }

    @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
        switch inputMode {
        case .touch:
            handleTouchPan(recognizer)
        case .trackpad:
            handleTrackpadPan(recognizer)
        }
    }

    private func handleTouchPan(_ recognizer: UIPanGestureRecognizer) {
        let location = recognizer.location(in: self)
        guard let normalized = normalizedFor(location) else { return }
        let mapped = mapToFullFramebuffer(normalized: normalized)
        switch recognizer.state {
        case .began:
            touchPanLastNormalized = mapped
            touchPanActiveButtonDown = true
            controller?.pointerDown(.left, atNormalized: mapped)
        case .changed:
            touchPanLastNormalized = mapped
            controller?.pointerMove(toNormalized: mapped)
        case .ended, .cancelled, .failed:
            if touchPanActiveButtonDown {
                touchPanActiveButtonDown = false
                let endNormalized = touchPanLastNormalized ?? mapped
                controller?.pointerUp(.left, atNormalized: endNormalized)
            }
            touchPanLastNormalized = nil
        default:
            break
        }
    }

    private func handleTrackpadPan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: self)
        switch recognizer.state {
        case .began:
            trackpadPanStartCursor = trackpadCursorNormalized
            trackpadPanStartPoint = .zero
        case .changed:
            guard let start = trackpadPanStartCursor else { return }
            let fbSize = framebufferContentSize()
            guard let displayed = inputMapper.displayedRect(for: fbSize, in: bounds.size),
                  displayed.width > 0, displayed.height > 0 else { return }
            let dx = translation.x / displayed.width
            let dy = translation.y / displayed.height
            let newCursor = CGPoint(
                x: max(0, min(1, start.x + dx)),
                y: max(0, min(1, start.y + dy))
            )
            trackpadCursorNormalized = newCursor
            onTrackpadCursorChanged?(newCursor)
            controller?.pointerMove(toNormalized: mapToFullFramebuffer(normalized: newCursor))
        case .ended, .cancelled, .failed:
            trackpadPanStartCursor = nil
            trackpadPanStartPoint = nil
        default: break
        }
    }

    @objc private func handleTwoFingerPan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: self)
        recognizer.setTranslation(.zero, in: self)
        scrollAccumulator.x += translation.x
        scrollAccumulator.y += translation.y

        emitScrollEvents()
        if recognizer.state == .ended || recognizer.state == .cancelled {
            scrollAccumulator = .zero
        }
    }

    @objc private func handleIndirectScroll(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: self)
        recognizer.setTranslation(.zero, in: self)
        scrollAccumulator.x += translation.x
        scrollAccumulator.y += translation.y
        emitScrollEvents()
        if recognizer.state == .ended || recognizer.state == .cancelled {
            scrollAccumulator = .zero
        }
    }

    private func emitScrollEvents() {
        guard let controller else { return }
        let cursor: CGPoint = inputMode == .trackpad
            ? trackpadCursorNormalized
            : (indirectPointerNormalized ?? CGPoint(x: 0.5, y: 0.5))
        let mapped = mapToFullFramebuffer(normalized: cursor)
        let stepsY = Int((scrollAccumulator.y / Self.scrollStepPoints).rounded(.towardZero))
        if stepsY != 0 {
            let dir: ScrollDirection = stepsY > 0 ? .down : .up
            controller.pointerScroll(dir, steps: UInt32(abs(stepsY)), atNormalized: mapped)
            scrollAccumulator.y -= CGFloat(stepsY) * Self.scrollStepPoints
        }
        let stepsX = Int((scrollAccumulator.x / Self.scrollStepPoints).rounded(.towardZero))
        if stepsX != 0 {
            let dir: ScrollDirection = stepsX > 0 ? .right : .left
            controller.pointerScroll(dir, steps: UInt32(abs(stepsX)), atNormalized: mapped)
            scrollAccumulator.x -= CGFloat(stepsX) * Self.scrollStepPoints
        }
    }

    @objc private func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
        switch recognizer.state {
        case .began:
            pinchStartScale = zoomScale
            pinchStartTranslation = contentTranslation
        case .changed:
            let newScale = max(0.5, min(8.0, pinchStartScale * recognizer.scale))
            zoomScale = newScale
        case .ended, .cancelled, .failed:
            // snap back if zoomed too much
            if zoomScale < 1.01 {
                zoomScale = 1.0
                contentTranslation = .zero
            }
        default: break
        }
    }

    @objc private func handleHover(_ recognizer: UIHoverGestureRecognizer) {
        let location = recognizer.location(in: self)
        guard let normalized = inputMapper.normalize(viewPoint: location,
                                                     in: bounds.size,
                                                     framebufferSize: framebufferContentSize()) else { return }
        indirectPointerNormalized = normalized
        if recognizer.state == .changed || recognizer.state == .began {
            controller?.pointerMove(toNormalized: mapToFullFramebuffer(normalized: normalized))
        }
    }

    private func normalizedFor(_ point: CGPoint) -> CGPoint? {
        inputMapper.normalize(viewPoint: point,
                              in: bounds.size,
                              framebufferSize: framebufferContentSize())
    }

    private func mapToFullFramebuffer(normalized: CGPoint) -> CGPoint {
        guard let screen = selectedScreen,
              let totalSize = controller?.framebufferSize,
              totalSize.width > 0, totalSize.height > 0 else {
            return normalized
        }
        let totalWidth = CGFloat(totalSize.width)
        let totalHeight = CGFloat(totalSize.height)
        let x = (screen.frame.origin.x + normalized.x * screen.frame.width) / totalWidth
        let y = (screen.frame.origin.y + normalized.y * screen.frame.height) / totalHeight
        return CGPoint(x: max(0, min(1, x)), y: max(0, min(1, y)))
    }

    // MARK: Hardware keyboard

    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        var handled = false
        for press in presses {
            guard let key = press.key else { continue }
            handleKey(key, isDown: true)
            handled = true
        }
        if !handled { super.pressesBegan(presses, with: event) }
    }

    override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        var handled = false
        for press in presses {
            guard let key = press.key else { continue }
            handleKey(key, isDown: false)
            handled = true
        }
        if !handled { super.pressesEnded(presses, with: event) }
    }

    override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        for press in presses {
            guard let key = press.key else { continue }
            handleKey(key, isDown: false)
        }
        super.pressesCancelled(presses, with: event)
    }

    private func handleKey(_ key: UIKey, isDown: Bool) {
        guard let controller else { return }
        let modifiers = KeyMapping.modifierKeysyms(for: key.modifierFlags)
        if isDown {
            for sym in modifiers { controller.keyDown(keysym: sym) }
        }
        if let sym = KeyMapping.keysym(for: key) {
            if isDown {
                controller.keyDown(keysym: sym)
            } else {
                controller.keyUp(keysym: sym)
            }
        }
        if !isDown {
            for sym in modifiers.reversed() { controller.keyUp(keysym: sym) }
        }
    }

    // MARK: Software keyboard accessory

    private func makeFunctionAccessoryView() -> UIView {
        let bar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44))
        bar.barStyle = .black
        bar.tintColor = .white
        bar.isTranslucent = true

        func barButton(title: String?, image: String?, action: Selector) -> UIBarButtonItem {
            if let title {
                return UIBarButtonItem(title: title, style: .plain, target: self, action: action)
            }
            return UIBarButtonItem(image: UIImage(systemName: image ?? ""),
                                   style: .plain,
                                   target: self,
                                   action: action)
        }

        let esc = barButton(title: "esc", image: nil, action: #selector(accessoryEsc))
        let tab = barButton(title: "tab", image: nil, action: #selector(accessoryTab))
        let ctrl = barButton(title: "ctrl", image: nil, action: #selector(accessoryControl))
        let cmd = barButton(title: "⌘", image: nil, action: #selector(accessoryCommand))
        let opt = barButton(title: "⌥", image: nil, action: #selector(accessoryOption))
        let left = barButton(title: nil, image: "arrow.left", action: #selector(accessoryLeft))
        let down = barButton(title: nil, image: "arrow.down", action: #selector(accessoryDown))
        let up = barButton(title: nil, image: "arrow.up", action: #selector(accessoryUp))
        let right = barButton(title: nil, image: "arrow.right", action: #selector(accessoryRight))
        let spacer = UIBarButtonItem.flexibleSpace()
        let dismiss = barButton(title: nil,
                                image: "keyboard.chevron.compact.down",
                                action: #selector(accessoryDismiss))
        bar.items = [esc, tab, ctrl, cmd, opt, spacer, left, down, up, right, spacer, dismiss]
        return bar
    }

    @objc private func accessoryEsc() { controller?.sendEscape() }
    @objc private func accessoryTab() { controller?.sendTab() }
    @objc private func accessoryControl() {
        controller?.pressKeyCombo([.control])
    }
    @objc private func accessoryCommand() {
        controller?.pressKeyCombo([.command])
    }
    @objc private func accessoryOption() {
        controller?.pressKeyCombo([.option])
    }
    @objc private func accessoryLeft() { controller?.sendArrow(.left) }
    @objc private func accessoryDown() { controller?.sendArrow(.down) }
    @objc private func accessoryUp() { controller?.sendArrow(.up) }
    @objc private func accessoryRight() { controller?.sendArrow(.right) }
    @objc private func accessoryDismiss() {
        setSoftwareKeyboardVisible(false)
    }
}

#endif
