"""
Utilities to deal with signature form fields and their properties in PDF files.
"""

import logging
from dataclasses import dataclass
from enum import Enum, Flag, unique
from typing import List, Optional, Set, Tuple, Union

from asn1crypto import x509
from asn1crypto.x509 import KeyUsage
from pyhanko.pdf_utils import generic
from pyhanko.pdf_utils.content import RawContent
from pyhanko.pdf_utils.generic import pdf_name, pdf_string
from pyhanko.pdf_utils.layout import BoxConstraints
from pyhanko.pdf_utils.misc import (
    OrderedEnum,
    PdfError,
    PdfReadError,
    PdfWriteError,
    get_and_apply,
    rd,
)
from pyhanko.pdf_utils.rw_common import PdfHandler
from pyhanko.pdf_utils.writer import BasePdfFileWriter
from pyhanko.sign.general import SigningError, UnacceptableSignerError

from pyhanko_certvalidator.authority import AuthorityWithCert
from pyhanko_certvalidator.errors import InvalidCertificateError
from pyhanko_certvalidator.path import ValidationPath

__all__ = [
    'SigFieldSpec',
    'SigSeedValFlags',
    'SigCertConstraints',
    'SigSeedValueSpec',
    'SigCertConstraintFlags',
    'SigSeedSubFilter',
    'SeedValueDictVersion',
    'SeedLockDocument',
    'SigCertKeyUsage',
    'MDPPerm',
    'FieldMDPAction',
    'FieldMDPSpec',
    'SignatureFormField',
    'InvisSigSettings',
    'VisibleSigSettings',
    'enumerate_sig_fields',
    'append_signature_field',
    'ensure_sig_flags',
    'prepare_sig_field',
    'apply_sig_field_spec_properties',
    'annot_width_height',
    'get_sig_field_annot',
]

logger = logging.getLogger(__name__)


# TODO support other seed value dict entries
# TODO add more customisability appearance-wise


class MDPPerm(OrderedEnum):
    """
    Indicates a ``/DocMDP`` level.

    Cf. Table 254  in ISO 32000-1.
    """

    NO_CHANGES = 1
    """
    No changes to the document are allowed.

    .. warning::
        This does not apply to DSS updates and the addition of document time
        stamps.
    """
    FILL_FORMS = 2
    """
    Form filling & signing is allowed.
    """

    ANNOTATE = 3
    """
    Form filling, signing and commenting are allowed.

    .. warning::
        Validating this ``/DocMDP`` level is not currently supported,
        but included in the list for completeness.
    """


class SeedSignatureType:
    """
    Signature type indicator to be embedded into the seed value dictionary
    attached to a signature field.

    :param mdp_perm:
        If not ``None``, indicates that the signature field is intended for
        a certification signature. The :class:`MDPPerm` value passed as the
        ``mdp_perm`` parameter indicates the modification policy that the
        certification signature should use.

        A value of ``None`` indicates that the signature field is intended for
        an approval signature (i.e. a non-certification signature).
    """

    def __init__(self, mdp_perm: Optional[MDPPerm] = None):
        self.mdp_perm = mdp_perm

    def __eq__(self, other):
        return (
            isinstance(other, SeedSignatureType)
            and other.mdp_perm == self.mdp_perm
        )

    def certification_signature(self) -> bool:
        return self.mdp_perm is not None


class SigSeedValFlags(Flag):
    """
    Flags for the ``/Ff`` entry in the seed value dictionary for a signature
    field. These mark which of the constraints are to be strictly enforced,
    as opposed to optional ones.

    .. warning::
        The flags :attr:`LEGAL_ATTESTATION` and :attr:`APPEARANCE_FILTER` are
        processed in accordance with the specification when creating a
        signature, but support is nevertheless limited.

        * PyHanko does not support legal attestations at all, so given that
          the :attr:`LEGAL_ATTESTATION` requirement flag only restricts the
          legal attestations that can be used by the signer, pyHanko can safely
          ignore it when signing.

          On the other hand, since the validator is not aware of
          legal attestations either, it cannot validate signatures that
          make :attr:`~.SigSeedValueSpec.legal_attestations` a mandatory
          constraint.
        * Since pyHanko does not define any named appearances, setting
          the :attr:`APPEARANCE_FILTER` flag and the
          :attr:`~.SigSeedValueSpec.appearance` entry in the seed value
          dictionary will make pyHanko refuse to sign the document.

          When validating, the situation is different: since pyHanko has no
          way of knowing whether the signer used the named appearance imposed
          by the seed value dictionary, it will simply emit a warning and
          continue validating the signature.
    """

    FILTER = 1
    """
    Makes the signature handler setting mandatory. PyHanko only supports
    ``/Adobe.PPKLite``.
    """

    SUBFILTER = 2
    """
    See :attr:`~.SigSeedValueSpec.subfilters`.
    """

    V = 4
    """
    See :attr:`~.SigSeedValueSpec.sv_dict_version`.
    """

    REASONS = 8
    """
    See :attr:`~.SigSeedValueSpec.reasons`.
    """

    LEGAL_ATTESTATION = 16
    """
    See :attr:`~.SigSeedValueSpec.legal_attestations`.
    """

    ADD_REV_INFO = 32
    """
    See :attr:`~.SigSeedValueSpec.add_rev_info`.
    """

    DIGEST_METHOD = 64
    """
    See :attr:`~.SigSeedValueSpec.digest_method`.
    """

    LOCK_DOCUMENT = 128
    """
    See :attr:`~.SigSeedValueSpec.lock_document`.
    """

    APPEARANCE_FILTER = 256
    """
    See :attr:`~.SigSeedValueSpec.appearance`.
    """


class SigCertConstraintFlags(Flag):
    """
    Flags for the ``/Ff`` entry in the certificate seed value dictionary for
    a dictionary field. These mark which of the constraints are to be
    strictly enforced, as opposed to optional ones.

    .. warning::
        While this enum records values for all flags, not all corresponding
        constraint types have been implemented yet.
    """

    SUBJECT = 1
    """
    See :attr:`SigCertConstraints.subjects`.
    """

    ISSUER = 2
    """
    See :attr:`SigCertConstraints.issuers`.
    """

    OID = 4
    """
    Currently not supported.
    """

    SUBJECT_DN = 8
    """ 
    See :attr:`SigCertConstraints.subject_dn`.
    """

    RESERVED = 16
    """
    Currently not supported (reserved).
    """

    KEY_USAGE = 32
    """
    See :attr:`SigCertConstraints.key_usage`.
    """

    URL = 64
    """
    See :attr:`SigCertConstraints.info_url`.
    
    .. note::
        As specified in the standard, this enforcement bit is supposed to be
        ignored by default. We include it for compatibility reasons.
    """

    UNSUPPORTED = RESERVED | OID
    """
    Flags for which the corresponding constraint is unsupported.
    """


class SigCertKeyUsage:
    """
    Encodes the key usage bits that must (resp. must not) be active on the
    signer's certificate.

    .. note::
        See § 4.2.1.3 in :rfc:`5280` and :class:`.KeyUsage` for more
        information on key usage extensions.

    .. note::
        The human-readable names of the key usage extensions are recorded
        in ``camelCase`` in :rfc:`5280`, but this class uses
        the naming convention of :class:`.KeyUsage` in ``asn1crypto``.
        The conversion is done by replacing ``camelCase`` with ``snake_case``.
        For example, ``nonRepudiation`` becomes ``non_repudiation``, and
        ``digitalSignature`` turns into ``digital_signature``.

    .. note::
        This class is intended to closely replicate the definition of the
        KeyUsage entry Table 235 in ISO 32000-1.
        In particular, it does *not* provide a mechanism to deal
        with extended key usage extensions (cf. § 4.2.1.12 in :rfc:`5280`).

    :param must_have:
        The :class:`.KeyUsage` object encoding the key usage extensions
        that must be present on the signer's certificate.
    :param forbidden:
        The :class:`.KeyUsage` object encoding the key usage extensions
        that must *not* be present on the signer's certificate.
    """

    def __init__(
        self,
        must_have: Optional[KeyUsage] = None,
        forbidden: Optional[KeyUsage] = None,
    ):
        self.must_have = must_have if must_have is not None else KeyUsage(set())
        self.forbidden = forbidden if forbidden is not None else KeyUsage(set())

    def encode_to_sv_string(self):
        """
        Encode the key usage requirements in the format specified in the PDF
        specification.

        :return:
            A string.
        """

        def fmt_bit(bit: int):
            if self.must_have[bit]:
                return '1'
            elif self.forbidden[bit]:
                return '0'
            else:
                return 'X'

        return ''.join(fmt_bit(bit) for bit in range(9))

    @classmethod
    def read_from_sv_string(cls, ku_str):
        """
        Parse a PDF KeyUsage string into an instance of
        :class:`.SigCertKeyUsage`. See Table 235 in ISO 32000-1.

        :param ku_str:
            A PDF KeyUsage string.
        :return:
            An instance of :class:`.SigCertKeyUsage`.
        """
        ku_str = ku_str[:9]

        def _as_tuple(with_val):
            return tuple(1 if val == with_val else 0 for val in ku_str)

        return SigCertKeyUsage(
            must_have=KeyUsage(_as_tuple('1')),
            forbidden=KeyUsage(_as_tuple('0')),
        )

    @classmethod
    def from_sets(
        cls,
        must_have: Optional[Set[str]] = None,
        forbidden: Optional[Set[str]] = None,
    ):
        """
        Initialise a :class:`.SigCertKeyUsage` object from two sets.

        :param must_have:
            The key usage extensions that must be present on the signer's
            certificate.
        :param forbidden:
            The key usage extensions that must *not* be present on the signer's
            certificate.
        :return:
            A :class:`.SigCertKeyUsage` object encoding these.
        """
        return SigCertKeyUsage(
            must_have=KeyUsage(set() if must_have is None else must_have),
            forbidden=KeyUsage(set() if forbidden is None else forbidden),
        )

    def must_have_set(self) -> Set[str]:
        """
        Return the set of key usage extensions that must be present
        on the signer's certificate.
        """
        return self.must_have.native

    def forbidden_set(self) -> Set[str]:
        """
        Return the set of key usage extensions that must not be present
        on the signer's certificate.
        """
        return self.forbidden.native

    def __eq__(self, other):
        return (
            isinstance(other, SigCertKeyUsage)
            and self.must_have_set() == other.must_have_set()
            and self.forbidden_set() == other.forbidden_set()
        )


name_type_abbrevs = {
    '2.5.4.3': 'CN',
    '2.5.4.5': 'SerialNumber',
    '2.5.4.6': 'C',
    '2.5.4.7': 'L',
    '2.5.4.8': 'ST',
    '2.5.4.10': 'O',
    '2.5.4.11': 'OU',
}

name_type_abbrevs_rev = {v: k for k, v in name_type_abbrevs.items()}


def x509_name_keyval_pairs(name: x509.Name, abbreviate_oids=False):
    rdns: x509.RDNSequence = name.chosen
    for rdn in rdns:
        for type_and_value in rdn:
            oid: x509.NameType = type_and_value['type']
            # these are all some kind of string, and the PDF
            # standard says that the value should be a text string object,
            # so we just have asn1crypto convert everything to strings
            value = type_and_value['value']
            key = oid.dotted
            if abbreviate_oids:
                key = name_type_abbrevs.get(key, key)

            yield key, value.native
            # these should be strings


@dataclass(frozen=True)
class SigCertConstraints:
    """
    This part of the seed value dictionary allows the document author
    to set constraints on the signer's certificate.

    See Table 235 in ISO 32000-1.
    """

    flags: SigCertConstraintFlags = SigCertConstraintFlags(0)
    """
    Enforcement flags. By default, all entries are optional.
    """

    subjects: Optional[List[x509.Certificate]] = None
    """
    Explicit list of certificates that can be used to sign a signature field.
    """

    subject_dn: Optional[x509.Name] = None
    """
    Certificate subject names that can be used to sign a signature field.
    Subject DN entries that are not mentioned are unconstrained.
    """

    issuers: Optional[List[x509.Certificate]] = None
    """
    List of issuer certificates that the signer certificate can be issued by.
    Note that these issuers do not need to be the *direct* issuer of the
    signer's certificate; any descendant relationship will do.
    """

    info_url: Optional[str] = None
    """
    Informational URL that should be opened when an appropriate certificate
    cannot be found (if :attr:`url_type` is ``/Browser``, that is).
    
    .. note::
        PyHanko ignores this value, but we include it for compatibility.
    """

    url_type: generic.NameObject = pdf_name('/Browser')
    """
    Handler that should be used to open :attr:`info_url`.
    ``/Browser`` is the only implementation-independent value.
    """

    key_usage: Optional[List[SigCertKeyUsage]] = None
    """
    Specify the key usage extensions that should (or should not) be present
    on the signer's certificate.
    """

    # TODO support OID constraints (certificate policies) and signature policy
    #  constraints.

    @classmethod
    def from_pdf_object(cls, pdf_dict):
        """
        Read a PDF dictionary into a :class:`.SigCertConstraints` object.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.SigCertConstraints` object.
        """

        if isinstance(pdf_dict, generic.IndirectObject):
            pdf_dict = pdf_dict.get_object()
        try:
            if pdf_dict['/Type'] != '/SVCert':  # pragma: nocover
                raise PdfReadError('Object /Type entry is not /SVCert')
        except KeyError:  # pragma: nocover
            pass
        flags = SigCertConstraintFlags(pdf_dict.get('/Ff', 0))
        subjects = [
            x509.Certificate.load(cert.original_bytes)
            for cert in pdf_dict.get('/Subject', ())
        ]
        issuers = [
            x509.Certificate.load(cert.original_bytes)
            for cert in pdf_dict.get('/Issuer', ())
        ]

        def format_attr(attr):
            # strip initial /
            attr = attr[1:]
            # attempt to convert abbreviated attrs to OIDs, since build()
            # takes OIDs
            return name_type_abbrevs_rev.get(attr.upper(), attr)

        subject_dns = x509.Name.build(
            {
                format_attr(attr): value
                for dn_dir in pdf_dict.get('/SubjectDN', ())
                for attr, value in dn_dir.items()
            }
        )

        def parse_key_usage(val):
            return [SigCertKeyUsage.read_from_sv_string(ku) for ku in val]

        key_usage = get_and_apply(pdf_dict, '/KeyUsage', parse_key_usage)

        url = pdf_dict.get('/URL')
        url_type = pdf_dict.get('/URLType')
        kwargs = {
            'flags': flags,
            'subjects': subjects or None,
            'subject_dn': subject_dns or None,
            'issuers': issuers or None,
            'info_url': url,
            'key_usage': key_usage,
        }
        if url is not None and url_type is not None:
            kwargs['url_type'] = url_type
        return cls(**kwargs)

    def as_pdf_object(self):
        """
        Render this :class:`.SigCertConstraints` object to a PDF dictionary.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """

        result = generic.DictionaryObject(
            {
                pdf_name('/Type'): pdf_name('/SVCert'),
                pdf_name('/Ff'): generic.NumberObject(self.flags.value),
            }
        )
        if self.subjects is not None:
            result[pdf_name('/Subject')] = generic.ArrayObject(
                generic.ByteStringObject(cert.dump()) for cert in self.subjects
            )
        if self.subject_dn:
            # FIXME Adobe Reader seems to ignore this for some reason.
            #  Should try to figure out what I'm doing wrong
            result[pdf_name('/SubjectDN')] = generic.ArrayObject(
                [
                    generic.DictionaryObject(
                        {
                            pdf_name('/' + key): pdf_string(value)
                            for key, value in x509_name_keyval_pairs(
                                self.subject_dn, abbreviate_oids=True
                            )
                        }
                    )
                ]
            )
        if self.issuers is not None:
            result[pdf_name('/Issuer')] = generic.ArrayObject(
                generic.ByteStringObject(cert.dump()) for cert in self.issuers
            )
        if self.info_url is not None:
            result[pdf_name('/URL')] = pdf_string(self.info_url)
            result[pdf_name('/URLType')] = self.url_type

        if self.key_usage is not None:
            result[pdf_name('/KeyUsage')] = generic.ArrayObject(
                pdf_string(ku.encode_to_sv_string()) for ku in self.key_usage
            )

        return result

    def satisfied_by(
        self,
        signer: x509.Certificate,
        validation_path: Optional[ValidationPath],
    ):
        """
        Evaluate whether a signing certificate satisfies the required
        constraints of this :class:`.SigCertConstraints` object.

        :param signer:
            The candidate signer's certificate.
        :param validation_path:
            Validation path of the signer's certificate.
        :raises UnacceptableSignerError:
            Raised if the conditions are not met.
        """
        # this function assumes that key usage & trust checks have
        #  passed already.
        flags = self.flags
        if flags & SigCertConstraintFlags.UNSUPPORTED:
            raise NotImplementedError(
                "Certificate constraint flags include mandatory constraints "
                "that are not supported."
            )
        if (
            flags & SigCertConstraintFlags.SUBJECT
        ) and self.subjects is not None:
            # Explicit whitelist of approved signer certificates
            # compare using issuer_serial
            acceptable = (s.issuer_serial for s in self.subjects)
            if signer.issuer_serial not in acceptable:
                raise UnacceptableSignerError(
                    "Signer certificate not on SVCert whitelist."
                )
        if (flags & SigCertConstraintFlags.ISSUER) and self.issuers is not None:
            if validation_path is None:
                raise UnacceptableSignerError("Validation path not provided.")
            # Here, we need to match any issuer in the chain of trust to
            #  any of the issuers on the approved list.

            # To do so, we collect all issuer_serial identifiers in the chain
            # for all certificates except the last one (i.e. the current signer)
            path_iss_serials = {
                authority.certificate.issuer_serial
                for authority in validation_path.iter_authorities()
                if isinstance(authority, AuthorityWithCert)
            }
            for issuer in self.issuers:
                if issuer.issuer_serial in path_iss_serials:
                    break
            else:
                # raise error if the loop runs to completion
                raise UnacceptableSignerError(
                    "Signer certificate cannot be traced back to approved "
                    "issuer."
                )
        if (flags & SigCertConstraintFlags.SUBJECT_DN) and self.subject_dn:
            # I'm not entirely sure whether my reading of the standard is
            #  is correct, but I believe that this is the intention:
            # A DistinguishedName object is a sequence of
            #  relative distinguished names (RDNs). The contents of the
            #  /SubjectDN specify a list of constraints that might apply to each
            #  of these RDNs. I believe the requirement is that each of the
            #  SubjectDN entries must match one of these RDNs.

            requirement_list = list(x509_name_keyval_pairs(self.subject_dn))
            subject_name = list(x509_name_keyval_pairs(signer.subject))
            if not all(attr in subject_name for attr in requirement_list):
                raise UnacceptableSignerError(
                    "Subject does not have some of the following required "
                    "attributes: " + self.subject_dn.human_friendly
                )
        if (
            flags & SigCertConstraintFlags.KEY_USAGE
        ) and self.key_usage is not None:
            from .validation.settings import KeyUsageConstraints

            for ku in self.key_usage:
                try:
                    KeyUsageConstraints(
                        key_usage=ku.must_have_set(),
                        key_usage_forbidden=ku.forbidden_set(),
                        # This is the way ISO 32k does things
                        match_all_key_usages=True,
                    ).validate(signer)
                    break
                except InvalidCertificateError:
                    continue
            else:
                raise UnacceptableSignerError(
                    "The signer satisfies none of the key usage "
                    "extension profiles specified in the seed value dictionary."
                )


@unique
class SigSeedSubFilter(Enum):
    """
    Enum declaring all supported ``/SubFilter`` values.
    """

    ADOBE_PKCS7_DETACHED = pdf_name("/adbe.pkcs7.detached")
    PADES = pdf_name("/ETSI.CAdES.detached")
    ETSI_RFC3161 = pdf_name("/ETSI.RFC3161")


@unique
class SigAuthType(Enum):
    """
    Enum declaring all supported ``/Prop_AuthType`` values.
    """

    PIN = pdf_string("PIN")
    PASSWORD = pdf_string("Password")
    FINGERPRINT = pdf_string("Fingerprint")


@unique
class SeedValueDictVersion(OrderedEnum):
    """
    Specify the minimal compliance level for a seed value dictionary processor.
    """

    PDF_1_5 = 1
    """
    Require the reader to understand all keys defined in PDF 1.5.
    """

    PDF_1_7 = 2
    """
    Require the reader to understand all keys defined in PDF 1.7.
    """

    PDF_2_0 = 3
    """
    Require the reader to understand all keys defined in PDF 2.0.
    """


@unique
class SeedLockDocument(Enum):
    """
    Provides a recommendation to the signer as to whether the document should
    be locked after signing.
    The corresponding flag in :attr:`.SigSeedValueSpec.flags` determines whether
    this constraint is a required constraint.
    """

    LOCK = pdf_name('/true')
    """
    Lock the document after signing.
    """

    DO_NOT_LOCK = pdf_name('/false')
    """
    Lock the document after signing.
    """

    SIGNER_DISCRETION = pdf_name('/auto')
    """
    Leave the decision up to the signer.
    
    .. note::
        This is functionally equivalent to not specifying any value.
    """


@dataclass(frozen=True)
class SigSeedValueSpec:
    """
    Python representation of a PDF seed value dictionary.
    """

    flags: SigSeedValFlags = SigSeedValFlags(0)
    """
    Enforcement flags. By default, all entries are optional.
    """

    reasons: Optional[List[str]] = None
    """
    Acceptable reasons for signing.
    """

    timestamp_server_url: Optional[str] = None
    """
    RFC 3161 timestamp server endpoint suggestion.
    """

    timestamp_required: bool = False
    """
    Flags whether a timestamp is required.
    This flag is only meaningful if :attr:`timestamp_server_url` is specified.
    """

    cert: Optional[SigCertConstraints] = None
    """
    Constraints on the signer's certificate.
    """

    subfilters: Optional[List[SigSeedSubFilter]] = None
    """
    Acceptable ``/SubFilter`` values.
    """

    digest_methods: Optional[List[str]] = None
    """
    Acceptable digest methods.
    """

    add_rev_info: Optional[bool] = None
    """
    Indicates whether revocation information should be embedded.
    
    .. warning::
        This flag exclusively refers to the Adobe-style revocation information
        embedded within the CMS object that is written to the signature field.
        PAdES-style revocation information that is saved to the document
        security store (DSS) does *not* satisfy the requirement.
        Additionally, the standard mandates that ``/SubFilter`` be equal to
        ``/adbe.pkcs7.detached`` if this flag is ``True``.
    """

    seed_signature_type: Optional[SeedSignatureType] = None
    """
    Specifies the type of signature that should occupy a signature field;
    this represents the ``/MDP`` entry in the seed value dictionary.
    See :class:`.SeedSignatureType` for details.
    
    .. caution::
        Since a certification-type signature is by definition the first
        signature applied to a document, compliance with this requirement
        cannot be cryptographically enforced.
    """

    sv_dict_version: Union[SeedValueDictVersion, int, None] = None
    """
    Specifies the compliance level required of a seed value dictionary
    processor. If ``None``, pyHanko will compute an appropriate value.
    
    .. note::
        You may also specify this value directly as an integer.
        This covers potential future versions of the standard that pyHanko
        does not support out of the box.
    """

    legal_attestations: Optional[List[str]] = None
    """
    Specifies the possible legal attestations that a certification signature
    occupying this signature field can supply.
    The corresponding flag in :attr:`flags` indicates whether this is a
    mandatory constraint.
    
    .. caution::
        Since :attr:`legal_attestations` is only relevant for certification
        signatures, compliance with this requirement cannot be reliably 
        enforced.
        Regardless, since pyHanko's validator is also unaware of legal
        attestation settings, it will refuse to validate signatures
        where this seed value constitutes a mandatory constraint.
        
        Additionally, since pyHanko does not support legal attestation
        specifications at all, it vacuously satisfies the requirements of this
        entry no matter what, and will therefore ignore it when signing.
    """

    lock_document: Optional[SeedLockDocument] = None
    """
    Tell the signer whether or not the document should be locked after signing
    this field; see :class:`.SeedLockDocument` for details.
    
    The corresponding flag in :attr:`flags` indicates whether this constraint
    is mandatory.
    """

    # TODO handle this value by reading named appearances from the user's
    #  settings

    appearance: Optional[str] = None
    """
    Specify a named appearance to use when generating the signature.
    The corresponding flag in :attr:`flags` indicates whether this constraint
    is mandatory.
    
    .. caution::
        There is no standard registry of named appearances, so these constraints
        are not portable, and cannot be validated.
        
        PyHanko currently does not define any named appearances.        
    """

    def as_pdf_object(self):
        """
        Render this :class:`.SigSeedValueSpec` object to a PDF dictionary.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """
        min_version = SeedValueDictVersion.PDF_1_5
        result = generic.DictionaryObject(
            {
                pdf_name('/Type'): pdf_name('/SV'),
                pdf_name('/Ff'): generic.NumberObject(self.flags.value),
            }
        )

        if self.subfilters is not None:
            result[pdf_name('/SubFilter')] = generic.ArrayObject(
                sf.value for sf in self.subfilters
            )
        if self.add_rev_info is not None:
            min_version = SeedValueDictVersion.PDF_1_7
            result[pdf_name('/AddRevInfo')] = generic.BooleanObject(
                self.add_rev_info
            )
        if self.digest_methods is not None:
            min_version = SeedValueDictVersion.PDF_1_7
            result[pdf_name('/DigestMethod')] = generic.ArrayObject(
                map(pdf_string, self.digest_methods)
            )
        if self.reasons is not None:
            result[pdf_name('/Reasons')] = generic.ArrayObject(
                pdf_string(reason) for reason in self.reasons
            )
        if self.timestamp_server_url is not None:
            min_version = SeedValueDictVersion.PDF_1_7
            result[pdf_name('/TimeStamp')] = generic.DictionaryObject(
                {
                    pdf_name('/URL'): pdf_string(self.timestamp_server_url),
                    pdf_name('/Ff'): generic.NumberObject(
                        1 if self.timestamp_required else 0
                    ),
                }
            )
        if self.cert is not None:
            result[pdf_name('/Cert')] = self.cert.as_pdf_object()
        if self.seed_signature_type is not None:
            mdp_perm = self.seed_signature_type.mdp_perm
            result[pdf_name('/MDP')] = generic.DictionaryObject(
                {
                    pdf_name('/P'): generic.NumberObject(
                        mdp_perm.value if mdp_perm is not None else 0
                    )
                }
            )
        if self.legal_attestations is not None:
            result[pdf_name('/LegalAttestation')] = generic.ArrayObject(
                pdf_string(att) for att in self.legal_attestations
            )
        if self.lock_document is not None:
            min_version = SeedValueDictVersion.PDF_2_0
            result[pdf_name('/LockDocument')] = self.lock_document.value
        if self.appearance is not None:
            result[pdf_name('/AppearanceFilter')] = pdf_string(self.appearance)

        specified_version = self.sv_dict_version
        if specified_version is not None:
            result[pdf_name('/V')] = generic.NumberObject(
                specified_version.value
                if isinstance(specified_version, SeedValueDictVersion)
                else specified_version
            )
        else:
            result[pdf_name('/V')] = generic.NumberObject(min_version.value)
        return result

    @classmethod
    def from_pdf_object(cls, pdf_dict):
        """
        Read from a seed value dictionary.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.SigSeedValueSpec` object.
        """
        if isinstance(pdf_dict, generic.IndirectObject):
            pdf_dict = pdf_dict.get_object()
        try:
            if pdf_dict['/Type'] != '/SV':  # pragma: nocover
                raise PdfReadError('Object /Type entry is not /SV')
        except KeyError:  # pragma: nocover
            pass

        flags = SigSeedValFlags(pdf_dict.get('/Ff', 0))
        try:
            sig_filter = pdf_dict['/Filter']
            if (flags & SigSeedValFlags.FILTER) and (
                sig_filter != '/Adobe.PPKLite'
            ):
                raise SigningError(
                    "Signature handler '%s' is not available, only the "
                    "default /Adobe.PPKLite is supported." % sig_filter
                )
        except KeyError:
            pass

        try:
            min_version = pdf_dict['/V']
            supported = SeedValueDictVersion.PDF_2_0.value
            if flags & SigSeedValFlags.V and min_version > supported:
                raise SigningError(
                    "Seed value dictionary version %s not supported."
                    % min_version
                )
            min_version = SeedValueDictVersion(min_version)
        except KeyError:
            min_version = None

        try:
            add_rev_info = bool(pdf_dict['/AddRevInfo'])
        except KeyError:
            add_rev_info = None

        subfilter_reqs = pdf_dict.get('/SubFilter', None)
        subfilters = None
        if subfilter_reqs is not None:

            def _subfilters():
                for s in subfilter_reqs:
                    try:
                        yield SigSeedSubFilter(s)
                    except ValueError:
                        pass

            subfilters = list(_subfilters())

        try:
            digest_methods = [s.lower() for s in pdf_dict['/DigestMethod']]
        except KeyError:
            digest_methods = None

        reasons = get_and_apply(pdf_dict, '/Reasons', list)
        legal_attestations = get_and_apply(pdf_dict, '/LegalAttestation', list)

        def read_mdp_dict(mdp):
            try:
                val = mdp['/P']
                return SeedSignatureType(None if val == 0 else MDPPerm(val))
            except (KeyError, TypeError, ValueError):
                raise SigningError(
                    f"/MDP entry {mdp} in seed value dictionary is not "
                    "correctly formatted."
                )

        signature_type = get_and_apply(pdf_dict, '/MDP', read_mdp_dict)

        def read_lock_document(val):
            try:
                return SeedLockDocument(val)
            except ValueError:
                raise SigningError(f"/LockDocument entry '{val}' is invalid.")

        lock_document = get_and_apply(
            pdf_dict, '/LockDocument', read_lock_document
        )
        appearance_filter = pdf_dict.get('/AppearanceFilter', None)
        timestamp_dict = pdf_dict.get('/TimeStamp', {})
        timestamp_server_url = timestamp_dict.get('/URL', None)
        timestamp_required = bool(timestamp_dict.get('/Ff', 0))
        cert_constraints = pdf_dict.get('/Cert', None)
        if cert_constraints is not None:
            cert_constraints = SigCertConstraints.from_pdf_object(
                cert_constraints
            )
        return cls(
            flags=flags,
            reasons=reasons,
            timestamp_server_url=timestamp_server_url,
            cert=cert_constraints,
            subfilters=subfilters,
            digest_methods=digest_methods,
            add_rev_info=add_rev_info,
            timestamp_required=timestamp_required,
            legal_attestations=legal_attestations,
            seed_signature_type=signature_type,
            sv_dict_version=min_version,
            lock_document=lock_document,
            appearance=appearance_filter,
        )

    def build_timestamper(self):
        """
        Return a timestamper object based on the :attr:`timestamp_server_url`
        attribute of this :class:`.SigSeedValueSpec` object.

        :return:
            A :class:`~.pyhanko.sign.timestamps.HTTPTimeStamper`.
        """
        from pyhanko.sign.timestamps import HTTPTimeStamper

        if self.timestamp_server_url:
            return HTTPTimeStamper(self.timestamp_server_url)


class FieldMDPAction(Enum):
    """
    Marker for the scope of a ``/FieldMDP`` policy.
    """

    ALL = pdf_name('/All')
    """
    The policy locks all form fields.
    """

    INCLUDE = pdf_name('/Include')
    """
    The policy locks all fields in the list (see :attr:`.FieldMDPSpec.fields`).
    """

    EXCLUDE = pdf_name('/Exclude')
    """
    The policy locks all fields except those specified in the list
    (see :attr:`.FieldMDPSpec.fields`).
    """


@dataclass(frozen=True)
class FieldMDPSpec:
    """``/FieldMDP`` policy description.

    This class models both field lock dictionaries and ``/FieldMDP``
    transformation parameters.
    """

    action: FieldMDPAction
    """
    Indicates the scope of the policy.
    """

    fields: Optional[List[str]] = None
    """
    Indicates the fields subject to the policy,
    unless :attr:`action` is :attr:`.FieldMDPAction.ALL`.
    """

    def as_pdf_object(self) -> generic.DictionaryObject:
        """
        Render this ``/FieldMDP`` policy description as a PDF dictionary.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """
        result = generic.DictionaryObject(
            {
                pdf_name('/Action'): self.action.value,
            }
        )
        if self.action != FieldMDPAction.ALL:
            result['/Fields'] = generic.ArrayObject(
                map(pdf_string, self.fields or ())
            )
        return result

    def as_transform_params(self) -> generic.DictionaryObject:
        """
        Render this ``/FieldMDP`` policy description as a PDF dictionary,
        ready for inclusion into the ``/TransformParams`` entry of a
        ``/FieldMDP`` dictionary associated with a signature object.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """

        result = self.as_pdf_object()
        result['/Type'] = pdf_name('/TransformParams')
        result['/V'] = pdf_name('/1.2')
        return result

    def as_sig_field_lock(self) -> generic.DictionaryObject:
        """
        Render this ``/FieldMDP`` policy description as a PDF dictionary,
        ready for inclusion into the ``/Lock`` dictionary of a signature field.

        :return:
            A :class:`~.generic.DictionaryObject`.
        """

        result = self.as_pdf_object()
        result['/Type'] = pdf_name('/SigFieldLock')
        return result

    @classmethod
    def from_pdf_object(cls, pdf_dict) -> 'FieldMDPSpec':
        """
        Read a PDF dictionary into a :class:`.FieldMDPSpec` object.

        :param pdf_dict:
            A :class:`~.generic.DictionaryObject`.
        :return:
            A :class:`.FieldMDPSpec` object.
        """
        try:
            action = FieldMDPAction(pdf_dict['/Action'])
        except KeyError:  # pragma: nocover
            raise PdfReadError("/Action is required.")

        if action != FieldMDPAction.ALL:
            try:
                fields = pdf_dict['/Fields']
            except KeyError:  # pragma: nocover
                raise PdfReadError(
                    "/Fields is required when /Action is not /All"
                )
        else:
            fields = None
        return cls(action=action, fields=fields)

    def is_locked(self, field_name: str) -> bool:
        """
        Adjudicate whether a field should be locked by the policy described by
        this :class:`.FieldMDPSpec` object.

        :param field_name:
            The name of a form field.
        :return:
            ``True`` if the field should be locked, ``False`` otherwise.
        """
        if self.action == FieldMDPAction.ALL:
            return True

        lock_result = self.action == FieldMDPAction.INCLUDE
        for scoped_field_name in self.fields or ():
            # treat non-terminal field in/exclusions as including the whole
            # tree beneath them
            if field_name.startswith(scoped_field_name):
                return lock_result
        return not lock_result


@dataclass(frozen=True)
class InvisSigSettings:
    """
    Invisible signature widget generation settings.

    These settings exist because there is no real way of including an untagged
    invisible signature in a document that complies with the requirements
    of both PDF/A-2 (or -3) and PDF/UA-1.

    Compatibility with PDF/A (the default) requires the print flag to be set.
    Compatibility with PDF/UA requires the hidden flag to be set (which is
    banned in PDF/A) or the box to be outside the crop box.
    """

    set_print_flag: bool = True
    """
    Set the print flag. Required in PDF/A.
    """

    set_hidden_flag: bool = False
    """
    Set the hidden flag. Required in PDF/UA.
    """

    box_out_of_bounds: bool = False
    """
    Put the box out of bounds (technically, this just makes the box
    zero-sized with large negative coordinates).

    This is a hack to get around the fact that PDF/UA requires the hidden
    flag to be set on all in-bounds untagged annotations, and some validators
    consider [0, 0, 0, 0] to be an in-bounds rectangle if (0, 0) is a point
    that falls within the crop box.
    """


@dataclass(frozen=True)
class VisibleSigSettings:
    """
    .. versionadded:: 0.14.0

    Additional flags used when setting up visible signature widgets.
    """

    rotate_with_page: bool = True
    """
    Allow the signature widget to rotate with the page if rotation is applied
    (e.g. by way of the page's ``/Rotate`` entry). Default is ``True``.

    .. note::
        If ``False``, this will cause the ``NoRotate`` flag to be set.
    """

    scale_with_page_zoom: bool = True
    """
    Allow the signature widget to scale with the page's zoom level.
    Default is ``True``.

    .. note::
        If ``False``, this will cause the ``NoZoom`` flag to be set.
    """

    print_signature: bool = True
    """
    Render the signature when the document is printed. Default ``True``.
    """


# TODO deal with fully qualified field names for the signature field


@dataclass(frozen=True)
class SigFieldSpec:
    """Description of a signature field to be created."""

    sig_field_name: str
    """
    Name of the signature field.
    """

    on_page: int = 0
    """
    Index of the page on which the signature field should be included (starting
    at `0`).
    A negative number counts pages from the back of the document,
    with index ``-1`` referring to the last page.
    
    .. note::
        This is essentially only relevant for visible signature fields, i.e.
        those that have a widget associated with them.
    """

    box: Optional[Tuple[int, int, int, int]] = None
    """
    Bounding box of the signature field, if applicable.

    Typically specified in ``ll_x``, ``ll_y``, ``ur_x``, ``ur_y`` format,
    where ``ll_*`` refers to the lower left and ``ur_*`` to the upper right
    corner.
    """

    seed_value_dict: Optional[SigSeedValueSpec] = None
    """
    Specification for the seed value dictionary, if applicable.
    """

    field_mdp_spec: Optional[FieldMDPSpec] = None
    """
    Specification for the field lock dictionary, if applicable.
    """

    doc_mdp_update_value: Optional[MDPPerm] = None
    """
    Value to use for the document modification policy associated with the
    signature in this field.
    
    This value will be embedded into the field lock dictionary if specified, and    
    is meaningless if :attr:`field_mdp_spec` is not specified.
    
    .. warning::
        DocMDP entries for approval signatures are a PDF 2.0 feature.
        Older PDF software will likely ignore this part of the field lock
        dictionary.
    """
    # TODO add a reference to the docs on certification once those are written.

    combine_annotation: bool = True
    """
    Flag controlling whether the field should be combined with its
    annotation dictionary; ``True`` by default.
    """

    empty_field_appearance: bool = False
    """
    Generate a neutral appearance stream for empty, visible signature fields.
    If ``False``, an empty appearance stream will be put in.
    
    .. note::
        We use an empty appearance stream to satisfy the appearance requirements
        for widget annotations in ISO 32000-2. However, even when a nontrivial
        appearance stream is present on an empty signature field, many viewers 
        will not use it to render the appearance of the empty field on-screen.

        Instead, these viewers typically substitute their own native widget.
    """

    invis_sig_settings: InvisSigSettings = InvisSigSettings()
    """
    Advanced settings to control invisible signature field generation.
    """

    readable_field_name: Optional[str] = None
    """
    Human-readable field name (``/TU`` entry).
    
    .. note::
        This value is commonly rendered as a tooltip in viewers, but also
        serves an accessibility purpose.
    """

    visible_sig_settings: VisibleSigSettings = VisibleSigSettings()
    """
    Advanced settings to control the generation of visible signature fields.
    """

    def format_lock_dictionary(self) -> Optional[generic.DictionaryObject]:
        if self.field_mdp_spec is None:
            return None
        result = self.field_mdp_spec.as_sig_field_lock()
        # this requires PDF 2.0 in principle, but meh, noncompliant
        # readers will ignore it anyway
        if self.doc_mdp_update_value is not None:
            result['/P'] = generic.NumberObject(self.doc_mdp_update_value.value)
        return result


def _insert_or_get_field_at(
    writer: BasePdfFileWriter,
    fields,
    path,
    parent_ref=None,
    modified=False,
    field_obj=None,
):
    current_partial, tail = path[0], path[1:]

    for field_ref in fields:
        assert isinstance(field_ref, generic.IndirectObject)
        field = field_ref.get_object()
        if field.get('/T', None) == current_partial:
            break
    else:
        # have to insert a new element into the fields array
        if field_obj is not None and not tail:
            field = field_obj
        else:
            # create a generic field
            field = generic.DictionaryObject()
        field['/T'] = pdf_string(current_partial)
        if parent_ref is not None:
            field['/Parent'] = parent_ref
        field_ref = writer.add_object(field)
        fields.append(field_ref)
        writer.update_container(fields)
        modified = True

    if not tail:
        return modified, field_ref
    # check for /Kids, and create it if necessary
    try:
        kids = field['/Kids']
    except KeyError:
        kids = field['/Kids'] = generic.ArrayObject()
        writer.update_container(field)
        modified = True

    # recurse in to /Kids array
    return _insert_or_get_field_at(
        writer,
        kids,
        tail,
        parent_ref=field_ref,
        modified=modified,
        field_obj=field_obj,
    )


def ensure_sig_flags(writer: BasePdfFileWriter, lock_sig_flags: bool = True):
    """
    Ensure the SigFlags setting is present in the AcroForm dictionary,
    and the /NeedAppearances setting is cleared if necessary.

    :param writer:
        A PDF writer.
    :param lock_sig_flags:
        Whether to flag the document as append-only.
    """
    # make sure /SigFlags is present. If not, create it
    # 3 = use append-only mode

    form = writer.root['/AcroForm']

    if lock_sig_flags:
        orig_sig_flags = form.get('/SigFlags', None)
        form['/SigFlags'] = generic.NumberObject(3)
        has_need_appearances = '/NeedAppearances' in form
        if has_need_appearances:
            del form['/NeedAppearances']
        if orig_sig_flags != 3 or has_need_appearances:
            writer.update_container(form)
    else:
        form.setdefault(pdf_name('/SigFlags'), generic.NumberObject(1))


def prepare_sig_field(
    sig_field_name,
    root,
    update_writer: BasePdfFileWriter,
    existing_fields_only=False,
    **kwargs,
):
    """
    Returns a tuple of a boolean and a reference to a signature field.
    The boolean is ``True`` if the field was created, and ``False`` otherwise.

    .. danger::
        This function is internal API.
    """

    try:
        form = root['/AcroForm']

        try:
            fields = form['/Fields']
        except KeyError:
            fields = form['/Fields'] = generic.ArrayObject()

        candidates = enumerate_sig_fields_in(
            fields, with_name=sig_field_name, refs_seen=set()
        )
        sig_field_ref = None
        try:
            field_name, value, sig_field_ref = next(candidates)
            if value is not None:
                raise SigningError(
                    'Signature field with name %s appears to be filled already.'
                    % sig_field_name
                )
        except StopIteration:
            if existing_fields_only:
                raise SigningError(
                    'No empty signature field with name %s found.'
                    % sig_field_name
                )
        form_created = False
    except KeyError:
        # we have to create the form
        if existing_fields_only:
            raise SigningError('This file does not contain a form.')
        # no AcroForm present, so create one
        form = generic.DictionaryObject()
        root[pdf_name('/AcroForm')] = update_writer.add_object(form)
        fields = generic.ArrayObject()
        form[pdf_name('/Fields')] = fields
        # now we need to mark the root as updated
        update_writer.update_root()
        form_created = True
        sig_field_ref = None

    if sig_field_ref is not None:
        return False, sig_field_ref

    # no signature field exists, so create one
    # default: grab a reference to the first page
    page_ref = update_writer.find_page_for_modification(0)[0]
    sig_form_kwargs = {'include_on_page': page_ref}
    sig_form_kwargs.update(**kwargs)
    sig_field = SignatureFormField(sig_field_name, **sig_form_kwargs)
    created, sig_field_ref = _insert_or_get_field_at(
        update_writer,
        fields,
        path=sig_field_name.split('.'),
        field_obj=sig_field,
    )
    sig_field.register_widget_annotation(update_writer, sig_field_ref)

    # if a field was added to an existing form, register an extra update
    if not form_created:
        update_writer.update_container(fields)
    return True, sig_field_ref


def get_sig_field_annot(
    sig_field: generic.DictionaryObject,
) -> generic.DictionaryObject:
    """
    Internal function to get the annotation of a signature field.

    :param sig_field:
        A signature field dictionary.
    :return:
        The dictionary of the corresponding annotation.
    """
    try:
        (sig_annot,) = sig_field['/Kids']
        sig_annot = sig_annot.get_object()
    except (ValueError, TypeError):
        raise SigningError(
            "Failed to access signature field's annotation. "
            "Signature field must have exactly one child annotation, "
            "or it must be combined with its annotation."
        )
    except KeyError:
        sig_annot = sig_field
    return sig_annot


def annot_width_height(
    annot_dict: generic.DictionaryObject,
) -> Tuple[float, float]:
    """
    Internal function to compute the width and height of an annotation.

    :param annot_dict:
        Annotation dictionary.
    :return:
        a (width, height) tuple
    """
    try:
        x1, y1, x2, y2 = annot_dict['/Rect']
    except KeyError:
        return 0, 0
    w = abs(x1 - x2)
    h = abs(y1 - y2)
    return w, h


def enumerate_sig_fields(
    handler: PdfHandler,
    filled_status: Optional[bool] = None,
    with_name: Optional[str] = None,
):
    """
    Enumerate signature fields.

    :param handler:
        The :class:`~.rw_common.PdfHandler` to operate on.
    :param filled_status:
        Optional boolean. If ``True`` (resp. ``False``) then all filled
        (resp. empty) fields are returned. If left ``None`` (the default), then
        all fields are returned.
    :param with_name:
        If not ``None``, only look for fields with the specified name.
    :return:
        A generator producing signature fields.
    """

    try:
        fields = handler.root['/AcroForm']['/Fields']
    except KeyError:
        return

    yield from enumerate_sig_fields_in(
        fields,
        filled_status=filled_status,
        with_name=with_name,
        refs_seen=set(),
    )


def enumerate_sig_fields_in(
    field_list,
    filled_status=None,
    with_name=None,
    parent_name="",
    parents=None,
    *,
    refs_seen,
):
    if not isinstance(field_list, generic.ArrayObject):
        logger.warning(
            f"Values of type {type(field_list)} are not valid as field "
            f"lists, must be array objects -- skipping."
        )
        return

    parents = parents or ()
    for field_ref in field_list:
        if not isinstance(field_ref, generic.IndirectObject):
            logger.warning(
                "Entries in field list must be indirect references -- skipping."
            )
            continue
        if field_ref.reference in refs_seen:
            raise PdfReadError("Circular reference in form tree")

        field = field_ref.get_object()
        if not isinstance(field, generic.DictionaryObject):
            logger.warning(
                "Entries in field list must be dictionary objects, not "
                f"{type(field)} -- skipping."
            )
            continue
        # /T is the field name. If not specified, we're dealing with a bare
        # widget, so skip it. (these should never occur in /Fields, but hey)
        try:
            field_name = field['/T']
        except KeyError:
            continue
        fq_name = (
            field_name
            if not parent_name
            else ("%s.%s" % (parent_name, field_name))
        )
        explicitly_requested = with_name is not None and fq_name == with_name
        child_requested = explicitly_requested or (
            with_name is not None and with_name.startswith(fq_name)
        )
        # /FT is inheritable, so go up the chain
        current_path = (field,) + parents
        for parent_field in current_path:
            try:
                field_type = parent_field['/FT']
                break
            except KeyError:
                continue
        else:
            field_type = None

        if field_type == '/Sig':
            field_value = field.get('/V')
            # "cast" to a regular string object
            filled = field_value is not None
            status_check = filled_status is None or filled == filled_status
            name_check = with_name is None or explicitly_requested
            if status_check and name_check:
                yield fq_name, field_value, field_ref
        elif explicitly_requested:
            raise SigningError(
                'Field with name %s exists but is not a signature field'
                % fq_name
            )

        # if necessary, descend into the field hierarchy
        if with_name is None or (child_requested and not explicitly_requested):
            try:
                yield from enumerate_sig_fields_in(
                    field['/Kids'],
                    parent_name=fq_name,
                    parents=current_path,
                    with_name=with_name,
                    filled_status=filled_status,
                    refs_seen=refs_seen | {field_ref.reference},
                )
            except KeyError:
                continue


def append_signature_field(
    pdf_out: BasePdfFileWriter, sig_field_spec: SigFieldSpec
):
    """
    Append signature fields to a PDF file.

    :param pdf_out:
        Incremental writer to house the objects.
    :param sig_field_spec:
        A :class:`.SigFieldSpec` object describing the signature field
        to add.
    """
    root = pdf_out.root

    page_ref = pdf_out.find_page_for_modification(sig_field_spec.on_page)[0]
    field_created, sig_field_ref = prepare_sig_field(
        sig_field_spec.sig_field_name,
        root,
        update_writer=pdf_out,
        existing_fields_only=False,
        box=sig_field_spec.box,
        include_on_page=page_ref,
        combine_annotation=sig_field_spec.combine_annotation,
        invis_settings=sig_field_spec.invis_sig_settings,
        visible_settings=sig_field_spec.visible_sig_settings,
    )
    ensure_sig_flags(writer=pdf_out, lock_sig_flags=False)
    if not field_created:
        raise PdfWriteError(
            'Signature field with name %s already exists.'
            % sig_field_spec.sig_field_name
        )

    sig_field = sig_field_ref.get_object()
    apply_sig_field_spec_properties(
        pdf_out, sig_field=sig_field, sig_field_spec=sig_field_spec
    )

    if sig_field_spec.box is not None:
        llx, lly, urx, ury = sig_field_spec.box
        w = abs(urx - llx)
        h = abs(ury - lly)
        if w and h:
            sig_field[pdf_name('/AP')] = ap_dict = generic.DictionaryObject()
            if sig_field_spec.empty_field_appearance:
                # draw a simple rectangle
                appearance_cmds = [
                    b'q',
                    # background
                    b'q 0.95 0.95 0.95 rg 0 0 %g %g re f Q' % (w, h),
                    # border
                    b'0.5 w 0 0 %g %g re S' % (w, h),
                    b'Q',
                ]
                ap_stream = RawContent(
                    b' '.join(appearance_cmds),
                    box=BoxConstraints(width=w, height=h),
                ).as_form_xobject()
            else:
                ap_stream = RawContent(
                    b'', box=BoxConstraints(width=w, height=h)
                ).as_form_xobject()
            ap_dict[pdf_name('/N')] = pdf_out.add_object(ap_stream)


def apply_sig_field_spec_properties(
    pdf_out: BasePdfFileWriter,
    sig_field: generic.DictionaryObject,
    sig_field_spec: SigFieldSpec,
):
    """
    Internal function to apply field spec properties to a newly created field.
    """

    if sig_field_spec.readable_field_name is not None:
        sig_field[pdf_name('/TU')] = generic.TextStringObject(
            sig_field_spec.readable_field_name
        )
    if sig_field_spec.seed_value_dict is not None:
        # /SV must be an indirect reference as per the spec
        sv_ref = pdf_out.add_object(
            sig_field_spec.seed_value_dict.as_pdf_object()
        )
        sig_field[pdf_name('/SV')] = sv_ref

    lock = sig_field_spec.format_lock_dictionary()
    if lock is not None:
        sig_field[pdf_name('/Lock')] = pdf_out.add_object(lock)


class SignatureFormField(generic.DictionaryObject):
    def __init__(
        self,
        field_name,
        *,
        box=None,
        include_on_page=None,
        combine_annotation=True,
        invis_settings: InvisSigSettings = InvisSigSettings(),
        visible_settings: VisibleSigSettings = VisibleSigSettings(),
        annot_flags=None,
    ):
        if box is not None:
            rect = [generic.FloatObject(rd(x)) for x in box]
            invisible = not (abs(box[0] - box[2]) and abs(box[1] - box[3]))
        else:
            coord = -9999 if invis_settings.box_out_of_bounds else 0
            rect = [generic.FloatObject(coord)] * 4
            invisible = True

        super().__init__(
            {
                # Signature field properties
                pdf_name('/FT'): pdf_name('/Sig'),
                pdf_name('/T'): pdf_string(field_name),
            }
        )

        self.combine_annotation = combine_annotation
        annot_dict: generic.DictionaryObject
        if combine_annotation:
            annot_dict = self
        else:
            annot_dict = generic.DictionaryObject()

        # Annotation properties: bare minimum
        annot_dict['/Type'] = pdf_name('/Annot')
        annot_dict['/Subtype'] = pdf_name('/Widget')

        if annot_flags is None:
            # this sets the "lock" bit
            annot_flags = 0b10000000
            if invisible:
                if invis_settings.set_hidden_flag:
                    annot_flags |= 0b10
                if invis_settings.set_print_flag:
                    annot_flags |= 0b100
            else:
                if visible_settings.print_signature:
                    annot_flags |= 0b100
                if not visible_settings.scale_with_page_zoom:
                    annot_flags |= 0b1000
                if not visible_settings.rotate_with_page:
                    annot_flags |= 0b10000

        annot_dict['/F'] = generic.NumberObject(annot_flags)
        annot_dict['/Rect'] = generic.ArrayObject(rect)

        self.page_ref = include_on_page
        if include_on_page is not None:
            annot_dict['/P'] = include_on_page

        self.annot_dict = annot_dict

    def register_widget_annotation(
        self, writer: BasePdfFileWriter, sig_field_ref
    ):
        annot_dict = self.annot_dict
        if not self.combine_annotation:
            annot_ref = writer.add_object(annot_dict)
            self['/Kids'] = generic.ArrayObject([annot_ref])
        else:
            annot_ref = sig_field_ref
        writer.register_annotation(self.page_ref, annot_ref)
