"""
smartscribe_docx.py  —  Python binding for SmartScribeDocx.dll
ctypes-based, zero compile step, works on any Python 3.7+.

Usage:
    from smartscribe_docx import DocxDocument, HF_DEFAULT, HF_EVEN, HF_FIRST

    with DocxDocument("mybook.docx") as doc:
        xml = DocxDocument.build_hf_xml(False, "Page {PAGE} of {NUMPAGES}", "center")
        doc.set_footer_xml(HF_DEFAULT, xml)
        doc.set_doc_property("title", "My Novel")
        doc.save()
"""

import ctypes
import ctypes.util
import os
from pathlib import Path
from typing import Optional, Dict

HF_DEFAULT = 0
HF_EVEN    = 1
HF_FIRST   = 2

def _find_dll() -> str:
    candidates = [
        Path(__file__).parent / "SmartScribeDocx.dll",
        Path(__file__).parent.parent / "SmartScribeDocx.dll",
        Path(os.environ.get("SMARTSCRIBEDOCX_DIR", "")) / "SmartScribeDocx.dll",
    ]
    for p in candidates:
        if p.exists():
            # On Python 3.8+, explicitly add the DLL's directory so Windows
            # can find Qt6Core.dll, Qt6Gui.dll etc. that live alongside it.
            dll_dir = str(p.parent)
            if hasattr(os, "add_dll_directory"):
                os.add_dll_directory(dll_dir)
            else:
                # Python < 3.8 fallback: prepend to PATH
                os.environ["PATH"] = dll_dir + os.pathsep + os.environ.get("PATH", "")
            return str(p)
    found = ctypes.util.find_library("SmartScribeDocx")
    if found:
        return found
    raise FileNotFoundError(
        "SmartScribeDocx.dll not found. Set SMARTSCRIBEDOCX_DIR or place "
        "SmartScribeDocx.dll alongside this file.")

_lib: Optional[ctypes.CDLL] = None

def _get_lib() -> ctypes.CDLL:
    global _lib
    if _lib is not None:
        return _lib
    _lib = ctypes.CDLL(_find_dll())
    _setup_signatures(_lib)
    _lib.ssDocx_init()   # development mode — callers should call init_licensed() instead
    return _lib

def init_licensed(key: str) -> bool:
    """Initialize with a commercial license key.
    Returns True if the key is valid.  Call this once at application startup
    before creating any DocxDocument instances.  If not called, or if the key
    is invalid, a development notice is printed to stderr on each init."""
    global _lib
    if _lib is None:
        _lib = ctypes.CDLL(_find_dll())
        _setup_signatures(_lib)
    return bool(_lib.ssDocx_init_licensed(key.encode('utf-8')))

def _setup_signatures(lib):
    vp = ctypes.c_void_p; cp = ctypes.c_char_p; ci = ctypes.c_int
    cu = ctypes.c_uint; u8p = ctypes.POINTER(ctypes.c_uint8); pip = ctypes.POINTER(ci)
    lib.ssDocx_init.restype = None;           lib.ssDocx_init.argtypes = []
    lib.ssDocx_init_licensed.restype = ci;    lib.ssDocx_init_licensed.argtypes = [cp]
    lib.ssDocx_version.restype = cp;       lib.ssDocx_version.argtypes = []
    lib.ssDocx_open.restype = vp;          lib.ssDocx_open.argtypes = [cp]
    lib.ssDocx_create_blank.restype = vp;  lib.ssDocx_create_blank.argtypes = [cp]
    lib.ssDocx_close.restype = None;       lib.ssDocx_close.argtypes = [vp]
    lib.ssDocx_is_valid.restype = ci;      lib.ssDocx_is_valid.argtypes = [vp]
    lib.ssDocx_error.restype = cp;         lib.ssDocx_error.argtypes = [vp]
    lib.ssDocx_save.restype = ci;          lib.ssDocx_save.argtypes = [vp]
    lib.ssDocx_save_as.restype = ci;       lib.ssDocx_save_as.argtypes = [vp, cp]
    lib.ssDocx_set_body_html.restype = ci;  lib.ssDocx_set_body_html.argtypes = [vp, cp]
    lib.ssDocx_write_html.restype = ci;      lib.ssDocx_write_html.argtypes = [cp, cp]
    lib.ssDocx_resave_full.restype = ci;     lib.ssDocx_resave_full.argtypes = [cp, cp]
    lib.ssDocx_get_margins.restype = ci;   lib.ssDocx_get_margins.argtypes = [vp]+[pip]*8
    lib.ssDocx_set_margins.restype = ci;   lib.ssDocx_set_margins.argtypes = [vp]+[ci]*8
    lib.ssDocx_get_header_xml.restype = u8p; lib.ssDocx_get_header_xml.argtypes = [vp,ci,ctypes.POINTER(cu)]
    lib.ssDocx_get_footer_xml.restype = u8p; lib.ssDocx_get_footer_xml.argtypes = [vp,ci,ctypes.POINTER(cu)]
    lib.ssDocx_set_header_xml.restype = ci;  lib.ssDocx_set_header_xml.argtypes = [vp,ci,u8p,cu]
    lib.ssDocx_set_footer_xml.restype = ci;  lib.ssDocx_set_footer_xml.argtypes = [vp,ci,u8p,cu]
    lib.ssDocx_build_hf_xml.restype = u8p;   lib.ssDocx_build_hf_xml.argtypes = [ci,cp,cp,ctypes.POINTER(cu)]
    lib.ssDocx_get_doc_property.restype = cp; lib.ssDocx_get_doc_property.argtypes = [vp,cp]
    lib.ssDocx_set_doc_property.restype = ci; lib.ssDocx_set_doc_property.argtypes = [vp,cp,cp]
    lib.ssDocx_get_title_page.restype = ci;   lib.ssDocx_get_title_page.argtypes = [vp]
    lib.ssDocx_set_title_page.restype = ci;   lib.ssDocx_set_title_page.argtypes = [vp,ci]
    lib.ssDocx_get_even_and_odd.restype = ci; lib.ssDocx_get_even_and_odd.argtypes = [vp]
    lib.ssDocx_set_even_and_odd.restype = ci; lib.ssDocx_set_even_and_odd.argtypes = [vp,ci]
    lib.ssDocx_set_page_number_format.restype = ci
    lib.ssDocx_set_page_number_format.argtypes = [vp,cp,ci]

class DocxDocument:
    """Python wrapper around SmartScribeDocx.dll."""
    TWIPS_PER_INCH = 1440

    def __init__(self, path: str):
        lib = _get_lib()
        self._lib = lib
        self._handle = lib.ssDocx_open(path.encode("utf-8"))
        if not self._handle:
            raise RuntimeError(f"Cannot open DOCX: {self._static_error()}")
        if not lib.ssDocx_is_valid(self._handle):
            raise RuntimeError(f"Invalid DOCX: {self._error()}")

    @classmethod
    def create_blank(cls, path: str) -> "DocxDocument":
        lib = _get_lib(); obj = cls.__new__(cls); obj._lib = lib
        obj._handle = lib.ssDocx_create_blank(path.encode("utf-8"))
        if not obj._handle: raise RuntimeError(f"Cannot create: {obj._static_error()}")
        return obj

    def close(self):
        if self._handle: self._lib.ssDocx_close(self._handle); self._handle = None
    def __enter__(self): return self
    def __exit__(self, *_): self.close()
    def __del__(self): self.close()

    def save(self):
        if not self._lib.ssDocx_save(self._handle): raise RuntimeError(self._error())

    def save_as(self, path: str):
        if not self._lib.ssDocx_save_as(self._handle, path.encode("utf-8")):
            raise RuntimeError(self._error())

    def set_body_html(self, html: str):
        """Replace the document body with content built from HTML
        (paragraphs + bold/italic/underline/font/size). Does NOT save -
        call save() or save_as() afterward, so you can set margins/headers too."""
        if not self._lib.ssDocx_set_body_html(self._handle, html.encode("utf-8")):
            raise RuntimeError(self._error())

    def get_margins(self) -> Dict[str, int]:
        vals = [ctypes.c_int(0) for _ in range(8)]
        self._lib.ssDocx_get_margins(self._handle, *[ctypes.byref(v) for v in vals])
        return dict(zip(["top","bottom","left","right","header_dist","footer_dist","gutter","mirror_margins"],
                        [v.value for v in vals]))

    def set_margins(self, top, bottom, left, right,
                    header_dist=720, footer_dist=720, gutter=0, mirror_margins=False):
        if not self._lib.ssDocx_set_margins(self._handle,
                top, bottom, left, right, header_dist, footer_dist, gutter, int(mirror_margins)):
            raise RuntimeError(self._error())

    def get_margins_inches(self) -> Dict[str, float]:
        T = self.TWIPS_PER_INCH
        return {k: v/T for k,v in self.get_margins().items() if k != "mirror_margins"}

    def set_margins_inches(self, top, bottom, left, right,
                            header_dist=0.5, footer_dist=0.5, gutter=0.0, mirror_margins=False):
        T = self.TWIPS_PER_INCH
        self.set_margins(int(top*T),int(bottom*T),int(left*T),int(right*T),
                         int(header_dist*T),int(footer_dist*T),int(gutter*T),mirror_margins)

    def get_header_xml(self, hf_type: int = HF_DEFAULT) -> Optional[bytes]:
        sz = ctypes.c_uint(0)
        p  = self._lib.ssDocx_get_header_xml(self._handle, hf_type, ctypes.byref(sz))
        return bytes(p[:sz.value]) if p and sz.value else None

    def get_footer_xml(self, hf_type: int = HF_DEFAULT) -> Optional[bytes]:
        sz = ctypes.c_uint(0)
        p  = self._lib.ssDocx_get_footer_xml(self._handle, hf_type, ctypes.byref(sz))
        return bytes(p[:sz.value]) if p and sz.value else None

    def set_header_xml(self, hf_type: int, xml: bytes):
        arr = (ctypes.c_uint8 * len(xml)).from_buffer_copy(xml)
        if not self._lib.ssDocx_set_header_xml(self._handle, hf_type, arr, len(xml)):
            raise RuntimeError(self._error())

    def set_footer_xml(self, hf_type: int, xml: bytes):
        arr = (ctypes.c_uint8 * len(xml)).from_buffer_copy(xml)
        if not self._lib.ssDocx_set_footer_xml(self._handle, hf_type, arr, len(xml)):
            raise RuntimeError(self._error())

    @staticmethod
    def build_hf_xml(is_header: bool, template: str, alignment: str = "center") -> bytes:
        """Build OOXML header/footer from template. Variables: {PAGE} {NUMPAGES} {TITLE} {CHAPTER} {AUTHOR}"""
        lib = _get_lib(); sz = ctypes.c_uint(0)
        p = lib.ssDocx_build_hf_xml(int(is_header), template.encode("utf-8"),
                                     alignment.encode("utf-8"), ctypes.byref(sz))
        if not p or sz.value == 0: raise RuntimeError("build_hf_xml failed")
        return bytes(p[:sz.value])

    def get_doc_property(self, key: str) -> str:
        r = self._lib.ssDocx_get_doc_property(self._handle, key.encode("utf-8"))
        return r.decode("utf-8") if r else ""

    def set_doc_property(self, key: str, value: str):
        if not self._lib.ssDocx_set_doc_property(self._handle,
                key.encode("utf-8"), value.encode("utf-8")):
            raise RuntimeError(self._error())

    def title_page(self) -> bool:  return bool(self._lib.ssDocx_get_title_page(self._handle))
    def set_title_page(self, on):   self._lib.ssDocx_set_title_page(self._handle, int(on))
    def even_and_odd(self) -> bool: return bool(self._lib.ssDocx_get_even_and_odd(self._handle))
    def set_even_and_odd(self, on): self._lib.ssDocx_set_even_and_odd(self._handle, int(on))
    def set_page_number_format(self, fmt: str, start: int = -1):
        self._lib.ssDocx_set_page_number_format(self._handle, fmt.encode("utf-8"), start)

    @staticmethod
    def write_html(path: str, html: str):
        """One-shot: create a blank DOCX at path, fill body from HTML, and save."""
        lib = _get_lib()
        if not lib.ssDocx_write_html(path.encode("utf-8"), html.encode("utf-8")):
            e = lib.ssDocx_error(None)
            raise RuntimeError(e.decode("utf-8") if e else "write_html failed")

    @staticmethod
    def resave_full(in_path: str, out_path: str):
        """Open an existing DOCX, render its full body through the engine, write to out_path.
        Effectively normalizes/rebuilds a document. Useful for round-trip testing."""
        lib = _get_lib()
        if not lib.ssDocx_resave_full(in_path.encode("utf-8"), out_path.encode("utf-8")):
            e = lib.ssDocx_error(None)
            raise RuntimeError(e.decode("utf-8") if e else "resave_full failed")

    def _error(self) -> str:
        e = self._lib.ssDocx_error(self._handle); return e.decode("utf-8") if e else "unknown"
    def _static_error(self) -> str:
        e = self._lib.ssDocx_error(None); return e.decode("utf-8") if e else "unknown"

    @staticmethod
    def version() -> str: return _get_lib().ssDocx_version().decode("utf-8")
    def __repr__(self): return f"<DocxDocument valid={bool(self._handle)}>"

def version() -> str:
    return DocxDocument.version()

def write_html(path: str, html: str):
    """One-shot module helper: create a blank DOCX at path, fill body from HTML, and save."""
    DocxDocument.write_html(path, html)
