import Foundation
import PDFKit

private func pdf_document_info(_ document: PDFDocument) -> [String: Any] {
    [
        "document_url": document.documentURL?.path as Any,
        "major_version": document.majorVersion,
        "minor_version": document.minorVersion,
        "is_encrypted": document.isEncrypted,
        "is_locked": document.isLocked,
        "permissions_status": document.permissionsStatus.rawValue,
        "access_permissions": document.accessPermissions.rawValue,
        "allows_printing": document.allowsPrinting,
        "allows_copying": document.allowsCopying,
        "allows_document_changes": document.allowsDocumentChanges,
        "allows_document_assembly": document.allowsDocumentAssembly,
        "allows_content_accessibility": document.allowsContentAccessibility,
        "allows_commenting": document.allowsCommenting,
        "allows_form_field_entry": document.allowsFormFieldEntry,
        "page_class": NSStringFromClass(document.pageClass),
    ]
}

private func pdf_document_attributes(_ document: PDFDocument) -> [String: Any] {
    let attributes = document.documentAttributes ?? [:]
    let formatter = ISO8601DateFormatter()
    return [
        "title": attributes[PDFDocumentAttribute.titleAttribute] as? String as Any,
        "author": attributes[PDFDocumentAttribute.authorAttribute] as? String as Any,
        "subject": attributes[PDFDocumentAttribute.subjectAttribute] as? String as Any,
        "creator": attributes[PDFDocumentAttribute.creatorAttribute] as? String as Any,
        "producer": attributes[PDFDocumentAttribute.producerAttribute] as? String as Any,
        "creation_date": (attributes[PDFDocumentAttribute.creationDateAttribute] as? Date).map(formatter.string(from:)) as Any,
        "modification_date": (attributes[PDFDocumentAttribute.modificationDateAttribute] as? Date).map(formatter.string(from:)) as Any,
        "keywords": attributes[PDFDocumentAttribute.keywordsAttribute] as? [String] as Any,
    ]
}

private func pdf_document_write_options(_ json: String?) throws -> [PDFDocumentWriteOption: Any] {
    guard let json, !json.isEmpty else { return [:] }
    guard let data = json.data(using: .utf8) else {
        throw PDFBridgeError.invalidArgument("invalid write options JSON")
    }
    guard let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
        throw PDFBridgeError.invalidArgument("invalid write options JSON")
    }

    var options: [PDFDocumentWriteOption: Any] = [:]

    if let ownerPassword = payload["owner_password"] as? String {
        options[PDFDocumentWriteOption.ownerPasswordOption] = ownerPassword
    }
    if let userPassword = payload["user_password"] as? String {
        options[PDFDocumentWriteOption.userPasswordOption] = userPassword
    }
    if let accessPermissions = payload["access_permissions"] as? NSNumber {
        if #available(macOS 12.0, *) {
            options[PDFDocumentWriteOption.accessPermissionsOption] = NSNumber(value: accessPermissions.uint64Value)
        } else {
            throw PDFBridgeError.framework("PDFDocumentAccessPermissionsOption requires macOS 12.0")
        }
    }
    if let burnInAnnotations = payload["burn_in_annotations"] as? Bool, burnInAnnotations {
        if #available(macOS 13.0, *) {
            options[PDFDocumentWriteOption.burnInAnnotationsOption] = NSNumber(value: true)
        } else {
            throw PDFBridgeError.framework("PDFDocumentBurnInAnnotationsOption requires macOS 13.0")
        }
    }
    if let saveTextFromOCR = payload["save_text_from_ocr"] as? Bool, saveTextFromOCR {
        if #available(macOS 13.0, *) {
            options[PDFDocumentWriteOption.saveTextFromOCROption] = NSNumber(value: true)
        } else {
            throw PDFBridgeError.framework("PDFDocumentSaveTextFromOCROption requires macOS 13.0")
        }
    }
    if let saveImagesAsJPEG = payload["save_images_as_jpeg"] as? Bool, saveImagesAsJPEG {
        if #available(macOS 13.4, *) {
            options[PDFDocumentWriteOption.saveImagesAsJPEGOption] = NSNumber(value: true)
        } else {
            throw PDFBridgeError.framework("PDFDocumentSaveImagesAsJPEGOption requires macOS 13.4")
        }
    }
    if let optimizeImagesForScreen = payload["optimize_images_for_screen"] as? Bool, optimizeImagesForScreen {
        if #available(macOS 13.4, *) {
            options[PDFDocumentWriteOption.optimizeImagesForScreenOption] = NSNumber(value: true)
        } else {
            throw PDFBridgeError.framework("PDFDocumentOptimizeImagesForScreenOption requires macOS 13.4")
        }
    }

    return options
}

@_cdecl("pdf_document_new")
public func pdf_document_new(
    _ outDocument: UnsafeMutablePointer<UnsafeMutableRawPointer?>?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let outDocument else {
            throw PDFBridgeError.invalidArgument("missing document output pointer")
        }
        outDocument.pointee = pdf_retain_document(PDFDocument())
    }
}

@_cdecl("pdf_document_new_with_url")
public func pdf_document_new_with_url(
    _ path: UnsafePointer<CChar>?,
    _ outDocument: UnsafeMutablePointer<UnsafeMutableRawPointer?>?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let path, let outDocument else {
            throw PDFBridgeError.invalidArgument("missing PDF path or output pointer")
        }
        let url = URL(fileURLWithPath: String(cString: path))
        guard let document = PDFDocument(url: url) else {
            throw PDFBridgeError.nullResult("PDFDocument(url:) returned nil")
        }
        outDocument.pointee = pdf_retain_document(document)
    }
}

@_cdecl("pdf_document_new_with_data")
public func pdf_document_new_with_data(
    _ bytes: UnsafePointer<UInt8>?,
    _ len: Int,
    _ outDocument: UnsafeMutablePointer<UnsafeMutableRawPointer?>?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let bytes, len > 0, let outDocument else {
            throw PDFBridgeError.invalidArgument("missing PDF bytes or output pointer")
        }
        let data = Data(bytes: bytes, count: len)
        guard let document = PDFDocument(data: data) else {
            throw PDFBridgeError.nullResult("PDFDocument(data:) returned nil")
        }
        outDocument.pointee = pdf_retain_document(document)
    }
}

@_cdecl("pdf_document_info_json")
public func pdf_document_info_json(_ handle: UnsafeMutableRawPointer?) -> UnsafeMutablePointer<CChar>? {
    guard let document = pdf_document_value(handle) else { return nil }
    return pdf_string(pdf_json_string(from: pdf_document_info(document)) ?? "{}")
}

@_cdecl("pdf_document_attributes_json")
public func pdf_document_attributes_json(_ handle: UnsafeMutableRawPointer?) -> UnsafeMutablePointer<CChar>? {
    guard let document = pdf_document_value(handle) else { return nil }
    return pdf_string(pdf_json_string(from: pdf_document_attributes(document)) ?? "{}")
}

@_cdecl("pdf_document_string")
public func pdf_document_string(_ handle: UnsafeMutableRawPointer?) -> UnsafeMutablePointer<CChar>? {
    guard let document = pdf_document_value(handle), let string = document.string else {
        return nil
    }
    return pdf_string(string)
}

@_cdecl("pdf_document_page_count")
public func pdf_document_page_count(_ handle: UnsafeMutableRawPointer?) -> UInt64 {
    guard let document = pdf_document_value(handle) else { return 0 }
    return UInt64(document.pageCount)
}

@_cdecl("pdf_document_page_at")
public func pdf_document_page_at(
    _ handle: UnsafeMutableRawPointer?,
    _ index: UInt64
) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(handle),
          index < UInt64(document.pageCount),
          let page = document.page(at: Int(index))
    else {
        return nil
    }
    return pdf_retain_page(page)
}

@_cdecl("pdf_document_index_for_page")
public func pdf_document_index_for_page(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ pageHandle: UnsafeMutableRawPointer?
) -> UInt64 {
    guard let document = pdf_document_value(documentHandle), let page = pdf_page_value(pageHandle) else {
        return UInt64.max
    }
    let index = document.index(for: page)
    return index == NSNotFound ? UInt64.max : UInt64(index)
}

@_cdecl("pdf_document_outline_root")
public func pdf_document_outline_root(_ handle: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(handle), let outline = document.outlineRoot else {
        return nil
    }
    return pdf_retain_outline(outline)
}

@_cdecl("pdf_document_set_outline_root")
public func pdf_document_set_outline_root(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ outlineHandle: UnsafeMutableRawPointer?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(documentHandle) else {
            throw PDFBridgeError.invalidArgument("missing document handle")
        }
        document.outlineRoot = pdf_outline_value(outlineHandle)
    }
}

@_cdecl("pdf_document_outline_item_for_selection")
public func pdf_document_outline_item_for_selection(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ selectionHandle: UnsafeMutableRawPointer?
) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(documentHandle),
          let selection = pdf_selection_value(selectionHandle),
          let outline = document.outlineItem(for: selection)
    else {
        return nil
    }
    return pdf_retain_outline(outline)
}

@_cdecl("pdf_document_selection_for_entire_document")
public func pdf_document_selection_for_entire_document(_ handle: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(handle), let selection = document.selectionForEntireDocument else {
        return nil
    }
    return pdf_retain_selection(selection)
}

@_cdecl("pdf_document_selection_from_pages_points")
public func pdf_document_selection_from_pages_points(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ startPageHandle: UnsafeMutableRawPointer?,
    _ startX: Double,
    _ startY: Double,
    _ endPageHandle: UnsafeMutableRawPointer?,
    _ endX: Double,
    _ endY: Double
) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(documentHandle),
          let startPage = pdf_page_value(startPageHandle),
          let endPage = pdf_page_value(endPageHandle)
    else {
        return nil
    }
    guard startPage.document === document, endPage.document === document else { return nil }
    let selection = document.selection(
        from: startPage,
        at: CGPoint(x: startX, y: startY),
        to: endPage,
        at: CGPoint(x: endX, y: endY)
    )
    return selection.map(pdf_retain_selection)
}

@_cdecl("pdf_document_selection_from_pages_points_with_granularity")
public func pdf_document_selection_from_pages_points_with_granularity(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ startPageHandle: UnsafeMutableRawPointer?,
    _ startX: Double,
    _ startY: Double,
    _ endPageHandle: UnsafeMutableRawPointer?,
    _ endX: Double,
    _ endY: Double,
    _ granularityRaw: UInt64
) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(documentHandle),
          let startPage = pdf_page_value(startPageHandle),
          let endPage = pdf_page_value(endPageHandle)
    else {
        return nil
    }
    guard startPage.document === document, endPage.document === document else { return nil }
    guard let granularity = PDFSelectionGranularity(rawValue: UInt(granularityRaw)) else {
        return nil
    }
    if #available(macOS 15.0, *) {
        let selection = document.selection(
            from: startPage,
            at: CGPoint(x: startX, y: startY),
            to: endPage,
            at: CGPoint(x: endX, y: endY),
            with: granularity
        )
        return selection.map(pdf_retain_selection)
    }
    return nil
}

@_cdecl("pdf_document_selection_from_pages_characters")
public func pdf_document_selection_from_pages_characters(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ startPageHandle: UnsafeMutableRawPointer?,
    _ startCharacter: UInt64,
    _ endPageHandle: UnsafeMutableRawPointer?,
    _ endCharacter: UInt64
) -> UnsafeMutableRawPointer? {
    guard let document = pdf_document_value(documentHandle),
          let startPage = pdf_page_value(startPageHandle),
          let endPage = pdf_page_value(endPageHandle)
    else {
        return nil
    }
    guard startPage.document === document, endPage.document === document else { return nil }
    let selection = document.selection(
        from: startPage,
        atCharacterIndex: Int(startCharacter),
        to: endPage,
        atCharacterIndex: Int(endCharacter)
    )
    return selection.map(pdf_retain_selection)
}

@_cdecl("pdf_document_unlock")
public func pdf_document_unlock(
    _ handle: UnsafeMutableRawPointer?,
    _ password: UnsafePointer<CChar>?
) -> Int32 {
    guard let document = pdf_document_value(handle), let password else {
        return 0
    }
    return document.unlock(withPassword: String(cString: password)) ? 1 : 0
}

@_cdecl("pdf_document_set_delegate")
public func pdf_document_set_delegate(
    _ handle: UnsafeMutableRawPointer?,
    _ delegateHandle: UnsafeMutableRawPointer?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(handle) else {
            throw PDFBridgeError.invalidArgument("missing document handle")
        }
        document.delegate = pdf_document_delegate_value(delegateHandle)
    }
}

@_cdecl("pdf_document_write_to_url")
public func pdf_document_write_to_url(
    _ handle: UnsafeMutableRawPointer?,
    _ path: UnsafePointer<CChar>?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(handle), let path else {
            throw PDFBridgeError.invalidArgument("missing document handle or output path")
        }
        let url = URL(fileURLWithPath: String(cString: path))
        guard document.write(to: url) else {
            throw PDFBridgeError.nullResult("PDFDocument.write(to:) returned false")
        }
    }
}

@_cdecl("pdf_document_write_to_url_with_options")
public func pdf_document_write_to_url_with_options(
    _ handle: UnsafeMutableRawPointer?,
    _ path: UnsafePointer<CChar>?,
    _ optionsJSON: UnsafePointer<CChar>?,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(handle), let path else {
            throw PDFBridgeError.invalidArgument("missing document handle or output path")
        }
        let options = try pdf_document_write_options(pdf_optional_string(optionsJSON))
        let url = URL(fileURLWithPath: String(cString: path))
        guard document.write(to: url, withOptions: options.isEmpty ? nil : options) else {
            throw PDFBridgeError.nullResult("PDFDocument.write(to:withOptions:) returned false")
        }
    }
}

@_cdecl("pdf_document_insert_page")
public func pdf_document_insert_page(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ pageHandle: UnsafeMutableRawPointer?,
    _ index: UInt64,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(documentHandle), let page = pdf_page_value(pageHandle) else {
            throw PDFBridgeError.invalidArgument("missing document or page handle")
        }
        guard index <= UInt64(document.pageCount) else {
            throw PDFBridgeError.invalidArgument("page index out of range")
        }
        document.insert(page, at: Int(index))
    }
}

@_cdecl("pdf_document_remove_page_at")
public func pdf_document_remove_page_at(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ index: UInt64,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(documentHandle) else {
            throw PDFBridgeError.invalidArgument("missing document handle")
        }
        guard index < UInt64(document.pageCount) else {
            throw PDFBridgeError.invalidArgument("page index out of range")
        }
        document.removePage(at: Int(index))
    }
}

@_cdecl("pdf_document_exchange_pages")
public func pdf_document_exchange_pages(
    _ documentHandle: UnsafeMutableRawPointer?,
    _ indexA: UInt64,
    _ indexB: UInt64,
    _ outError: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    pdf_run(outError) {
        guard let document = pdf_document_value(documentHandle) else {
            throw PDFBridgeError.invalidArgument("missing document handle")
        }
        guard indexA < UInt64(document.pageCount), indexB < UInt64(document.pageCount) else {
            throw PDFBridgeError.invalidArgument("page index out of range")
        }
        guard let pageA = document.page(at: Int(indexA)),
              let pageB = document.page(at: Int(indexB)),
              pageA.pageRef != nil,
              pageB.pageRef != nil
        else {
            throw PDFBridgeError.invalidArgument("PDFDocument.exchangePage is unsupported for synthetic in-memory pages")
        }
        document.exchangePage(at: Int(indexA), withPageAt: Int(indexB))
    }
}
