# SPDX-FileCopyrightText: 2026 geisserml <geisserml@gmail.com>
# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause

__all__ = ("PdfAttachment", )

import ctypes
from codecs import decode
import pypdfium2.raw as pdfium_c
import pypdfium2.internal as pdfium_i
from pypdfium2.internal import FPDF_WCHAR_size
from pypdfium2._helpers.misc import PdfiumError


class PdfAttachment (pdfium_i.AutoCastable):
    """
    Attachment helper class. See PDF 1.7, Section 7.11 "File Specifications".
    
    Attributes:
        raw (FPDF_ATTACHMENT):
            The underlying PDFium attachment handle.
        pdf (PdfDocument):
            Reference to the document this attachment belongs to. Must remain valid as long as the attachment is used.
    """
    
    # TODO consider using AutoCloseable machienery to guarantee `pdf` remains alive as long as the attachment object exists
    
    # Limitations of PDFium's attachment API: https://crbug.com/pdfium/893
    
    def __init__(self, raw, pdf):
        self.raw = raw
        self.pdf = pdf
    
    def get_name(self):
        """
        Returns:
            str: Name of the attachment.
        """
        n_bytes = pdfium_c.FPDFAttachment_GetName(self, None, 0)
        if not n_bytes:
            raise PdfiumError("Failed to get attachment name.")
        n_units = -(n_bytes // -FPDF_WCHAR_size)  # ceildiv
        buffer = (pdfium_c.FPDF_WCHAR * n_units)()
        pdfium_c.FPDFAttachment_GetName(self, buffer, n_bytes)
        return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
    
    def get_data(self):
        """
        Returns:
            ctypes.Array: The attachment's file data (as :class:`~ctypes.c_char` array).
        """
                
        n_bytes = ctypes.c_ulong()
        pdfium_c.FPDFAttachment_GetFile(self, None, 0, n_bytes)
        n_bytes = n_bytes.value
        if not n_bytes:
            raise PdfiumError(f"Failed to extract attachment (no data).")
        
        buffer = (ctypes.c_char * n_bytes)()
        out_buflen = ctypes.c_ulong()
        ok = pdfium_c.FPDFAttachment_GetFile(self, buffer, n_bytes, out_buflen)
        out_buflen = out_buflen.value
        if not ok:
            raise PdfiumError("Failed to extract attachment (error status).")
        if n_bytes < out_buflen:
            raise PdfiumError(f"Failed to extract attachment (expected {n_bytes} bytes, but got {out_buflen}).")
        
        return buffer
    
    def set_data(self, data):
        """
        Set the attachment's file data.
        If this function is called on an existing attachment, it will be changed to point at the new data, but the previous data will not be removed from the file (as of PDFium 5418).
        
        Parameters:
            data (bytes | ctypes.Array):
                New file data for the attachment. May be any data type that can be implicitly converted to :class:`~ctypes.c_void_p`.
        """
        ok = pdfium_c.FPDFAttachment_SetFile(self, self.pdf, data, len(data))
        if not ok:
            raise PdfiumError("Failed to set attachment data.")
    
    def has_key(self, key):
        """
        Parameters:
            key (str):
                A key to look for in the attachment's params dictionary.
        Returns:
            bool: True if *key* is contained in the params dictionary, False otherwise.
        """
        return pdfium_c.FPDFAttachment_HasKey(self, (key+"\x00").encode("utf-8"))
    
    def get_value_type(self, key):
        """
        Returns:
            int: Type of the value of *key* in the params dictionary (:attr:`FPDF_OBJECT_*`).
        """
        return pdfium_c.FPDFAttachment_GetValueType(self, (key+"\x00").encode("utf-8"))
    
    def get_str_value(self, key):
        """
        Returns:
            str: The value of *key* in the params dictionary, if it is a string or name.
            Otherwise, an empty string will be returned. On other failures, an exception will be raised.
        """
        
        enc_key = (key+"\x00").encode("utf-8")
        n_bytes = pdfium_c.FPDFAttachment_GetStringValue(self, enc_key, None, 0)
        if n_bytes <= 0:
            raise PdfiumError(f"Failed to get value of key '{key}'.")
        
        n_units = -(n_bytes // -FPDF_WCHAR_size)  # ceildiv
        buffer = (pdfium_c.FPDF_WCHAR * n_units)()
        pdfium_c.FPDFAttachment_GetStringValue(self, enc_key, buffer, n_bytes)
        
        return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
    
    def set_str_value(self, key, value):
        """
        Set the attribute specified by *key* to the string *value*.
        
        Parameters:
            value (str): New string value for the attribute.
        """
        enc_key = (key+"\x00").encode("utf-8")
        enc_value = (value+"\x00").encode("utf-16-le")
        enc_value_ptr = ctypes.cast(enc_value, pdfium_c.FPDF_WIDESTRING)
        ok = pdfium_c.FPDFAttachment_SetStringValue(self, enc_key, enc_value_ptr)
        if not ok:
            raise PdfiumError(f"Failed to set attachment param '{key}' to '{value}'.")
    
    # The interface of FPDFAttachment_GetSubtype() and FPDFAttachment_GetDescription() seems pretty much the same, just targeting different fields.
    
    def get_subtype(self):
        """
        Returns:
            str: The attachment's MIME type (``/Subtype``), as string.
            Returns an empty string if the attachment does not specify the subtype key.
        Hint:
            Checking the attached file's signature with ``libmagic`` may be a more reliable way of determining the MIME type; however, this implies calling :meth:`.get_data`.
        """
        n_bytes = pdfium_c.FPDFAttachment_GetSubtype(self, None, 0)  # including NUL
        if n_bytes == FPDF_WCHAR_size:
            return ""
        elif not n_bytes:
            raise PdfiumError("Failed to get attachment subtype")
        n_units = -(n_bytes // -FPDF_WCHAR_size)  # ceildiv
        buffer = (pdfium_c.FPDF_WCHAR * n_units)()
        pdfium_c.FPDFAttachment_GetSubtype(self, buffer, n_bytes)
        return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
    
    def get_desc(self):
        """
        Returns:
            str: The attachment's description, or an empty string if the attachment has no description.
        """
        n_bytes = pdfium_c.FPDFAttachment_GetDescription(self, None, 0)  # including NUL
        if n_bytes == FPDF_WCHAR_size:
            return ""
        elif not n_bytes:
            raise PdfiumError("Failed to get attachment description")
        n_units = -(n_bytes // -FPDF_WCHAR_size)  # ceildiv
        buffer = (pdfium_c.FPDF_WCHAR * n_units)()
        # NOTE The API unconditionally returns the same value as the previous call, and does not give feedback whether buffer has been actually modified or not.
        pdfium_c.FPDFAttachment_GetDescription(self, buffer, n_bytes)
        return decode(memoryview(buffer)[:n_units-1], "utf-16-le")
    
    def set_desc(self, string):
        """
        Parameters:
            string (str): Set the attachment description to this value.
        
        .. versionadded:: 5.14
            :meth:`.get_subtype`, :meth:`.get_desc`, :meth:`.set_desc`
        """
        enc_string = (string+"\x00").encode("utf-16-le")
        enc_string_ptr = ctypes.cast(enc_string, pdfium_c.FPDF_WIDESTRING)
        ok = pdfium_c.FPDFAttachment_SetDescription(self, enc_string_ptr)
        if not ok:
            raise PdfiumError("Failed to set attachment description")
