// SmartScribeDocx.cs  —  .NET P/Invoke wrapper for SmartScribeDocx.dll
// Compatible with .NET 6+ and .NET Framework 4.7+.
// No compile dependencies beyond the standard BCL.
//
// Usage:
//   using SmartScribe;
//
//   using var doc = new DocxDocument("mybook.docx");
//   var xml = DocxDocument.BuildHfXml(isHeader: false,
//                                      template: "Page {PAGE} of {NUMPAGES}",
//                                      alignment: "center");
//   doc.SetFooterXml(HfType.Default, xml);
//   doc.SetDocProperty("title", "My Novel");
//   doc.Save();

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace SmartScribe
{
    // ── HFType ────────────────────────────────────────────────────────────────
    public enum HfType
    {
        Default = 0,   // Odd pages (or all pages when even/odd off)
        Even    = 1,   // Even pages
        First   = 2,   // First page only
    }

    // ── Margins ───────────────────────────────────────────────────────────────
    public struct DocxMargins
    {
        public int Top, Bottom, Left, Right;
        public int HeaderDist, FooterDist, Gutter;
        public bool MirrorMargins;

        public const double TwipsPerInch = 1440.0;
        public double TopInches    => Top    / TwipsPerInch;
        public double BottomInches => Bottom / TwipsPerInch;
        public double LeftInches   => Left   / TwipsPerInch;
        public double RightInches  => Right  / TwipsPerInch;
    }

    // ── Native P/Invoke layer (internal) ─────────────────────────────────────
    internal static class Native
    {
        private const string DLL = "SmartScribeDocx";

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern void ssDocx_init();

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern int ssDocx_init_licensed(
            [MarshalAs(UnmanagedType.LPStr)] string key);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr ssDocx_version();

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern IntPtr ssDocx_open([MarshalAs(UnmanagedType.LPStr)] string path);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern IntPtr ssDocx_create_blank([MarshalAs(UnmanagedType.LPStr)] string path);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern void ssDocx_close(IntPtr h);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_is_valid(IntPtr h);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr ssDocx_error(IntPtr h);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_save(IntPtr h);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern int ssDocx_save_as(IntPtr h,
            [MarshalAs(UnmanagedType.LPStr)] string path);

        // HTML body functions use byte[] to avoid LPUTF8Str (.NET 7+ only).
        // Callers pass Encoding.UTF8.GetBytes(str) + null terminator.
        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_body_html(IntPtr h, [In] byte[] htmlUtf8);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_write_html([In] byte[] pathUtf8, [In] byte[] htmlUtf8);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_resave_full([In] byte[] inPathUtf8, [In] byte[] outPathUtf8);

        public static byte[] ToUtf8(string s)
        {
            var bytes = Encoding.UTF8.GetBytes(s);
            var result = new byte[bytes.Length + 1]; // null terminator
            bytes.CopyTo(result, 0);
            return result;
        }

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_get_margins(IntPtr h,
            out int top, out int bottom, out int left, out int right,
            out int headerDist, out int footerDist, out int gutter, out int mirror);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_margins(IntPtr h,
            int top, int bottom, int left, int right,
            int headerDist, int footerDist, int gutter, int mirror);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr ssDocx_get_header_xml(IntPtr h, int hfType, out uint size);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr ssDocx_get_footer_xml(IntPtr h, int hfType, out uint size);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_header_xml(IntPtr h, int hfType,
            [In] byte[] xml, uint size);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_footer_xml(IntPtr h, int hfType,
            [In] byte[] xml, uint size);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern IntPtr ssDocx_build_hf_xml(
            int isHeader,
            [MarshalAs(UnmanagedType.LPStr)] string templateText,
            [MarshalAs(UnmanagedType.LPStr)] string alignment,
            out uint size);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern IntPtr ssDocx_get_doc_property(IntPtr h,
            [MarshalAs(UnmanagedType.LPStr)] string key);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern int ssDocx_set_doc_property(IntPtr h,
            [MarshalAs(UnmanagedType.LPStr)] string key,
            [MarshalAs(UnmanagedType.LPStr)] string value);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_get_title_page(IntPtr h);
        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_title_page(IntPtr h, int on);
        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_get_even_and_odd(IntPtr h);
        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl)]
        public static extern int ssDocx_set_even_and_odd(IntPtr h, int on);

        [DllImport(DLL, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        public static extern int ssDocx_set_page_number_format(IntPtr h,
            [MarshalAs(UnmanagedType.LPStr)] string fmt, int start);

        public static string PtrToStringUtf8(IntPtr ptr)
        {
            if (ptr == IntPtr.Zero) return string.Empty;
            int len = 0;
            while (Marshal.ReadByte(ptr, len) != 0) len++;
            byte[] buf = new byte[len];
            Marshal.Copy(ptr, buf, 0, len);
            return Encoding.UTF8.GetString(buf);
        }

        public static byte[] PtrToBytes(IntPtr ptr, uint size)
        {
            if (ptr == IntPtr.Zero || size == 0) return null;
            byte[] buf = new byte[size];
            Marshal.Copy(ptr, buf, 0, (int)size);
            return buf;
        }
    }

    // ── Public DocxDocument class ─────────────────────────────────────────────
    public sealed class DocxDocument : IDisposable
    {
        private IntPtr _handle;
        private static bool _initialized;

        static DocxDocument()
        {
            // Development mode init — call InitLicensed() before first use
            // in production to suppress the stderr notice.
            if (!_initialized) { Native.ssDocx_init(); _initialized = true; }
        }

        /// <summary>
        /// Initialize with a commercial license key.
        /// Call this once at application startup, before creating any DocxDocument.
        /// Returns true if the key is valid (no notice printed to stderr).
        /// If not called, or if the key is invalid, a one-line notice is printed
        /// to stderr on each init — the library functions normally either way.
        /// </summary>
        public static bool InitLicensed(string key)
        {
            _initialized = true;
            return Native.ssDocx_init_licensed(key) != 0;
        }

        public DocxDocument(string path)
        {
            _handle = Native.ssDocx_open(path);
            if (_handle == IntPtr.Zero)
                throw new InvalidOperationException($"Cannot open DOCX: {StaticError()}");
            if (Native.ssDocx_is_valid(_handle) == 0)
                throw new InvalidOperationException($"Invalid DOCX: {LastError()}");
        }

        public static DocxDocument CreateBlank(string path)
        {
            var obj = new DocxDocument();
            obj._handle = Native.ssDocx_create_blank(path);
            if (obj._handle == IntPtr.Zero)
                throw new InvalidOperationException($"Cannot create DOCX: {StaticError()}");
            return obj;
        }
        private DocxDocument() { }

        public void Dispose()
        {
            if (_handle != IntPtr.Zero) { Native.ssDocx_close(_handle); _handle = IntPtr.Zero; }
        }

        // ── Save ──────────────────────────────────────────────────────────────
        public void Save()
        {
            if (Native.ssDocx_save(_handle) == 0) throw new InvalidOperationException(LastError());
        }
        public void SaveAs(string path)
        {
            if (Native.ssDocx_save_as(_handle, path) == 0)
                throw new InvalidOperationException(LastError());
        }

        // Body content (authoring)
        /// <summary>
        /// Replace the document body with content built from HTML
        /// (paragraphs + bold/italic/underline/font/size). Does NOT save -
        /// call Save() or SaveAs() afterward, so you can set margins/headers too.
        /// </summary>
        public void SetBodyHtml(string html)
        {
            if (Native.ssDocx_set_body_html(_handle, Native.ToUtf8(html)) == 0)
                throw new InvalidOperationException(LastError());
        }

        /// <summary>
        /// One-shot: create a blank DOCX at path, fill its body from HTML, and save.
        /// </summary>
        public static void WriteHtml(string path, string html)
        {
            if (Native.ssDocx_write_html(Native.ToUtf8(path), Native.ToUtf8(html)) == 0)
                throw new InvalidOperationException(StaticError());
        }

        /// <summary>
        /// Open an existing DOCX, render its full body through the engine,
        /// and write the result to outPath. Normalizes/rebuilds the document.
        /// </summary>
        public static void ResaveFull(string inPath, string outPath)
        {
            if (Native.ssDocx_resave_full(Native.ToUtf8(inPath), Native.ToUtf8(outPath)) == 0)
                throw new InvalidOperationException(StaticError());
        }

        // ── Margins ───────────────────────────────────────────────────────────
        public DocxMargins GetMargins()
        {
            Native.ssDocx_get_margins(_handle,
                out int t, out int b, out int l, out int r,
                out int hd, out int fd, out int g, out int m);
            return new DocxMargins { Top=t, Bottom=b, Left=l, Right=r,
                                     HeaderDist=hd, FooterDist=fd, Gutter=g,
                                     MirrorMargins=(m!=0) };
        }
        public void SetMargins(DocxMargins m)
        {
            Native.ssDocx_set_margins(_handle,
                m.Top, m.Bottom, m.Left, m.Right,
                m.HeaderDist, m.FooterDist, m.Gutter, m.MirrorMargins ? 1 : 0);
        }
        public void SetMarginsInches(double top, double bottom, double left, double right,
                                      double headerDist=0.5, double footerDist=0.5,
                                      double gutter=0, bool mirrorMargins=false)
        {
            const double T = DocxMargins.TwipsPerInch;
            SetMargins(new DocxMargins {
                Top=(int)(top*T), Bottom=(int)(bottom*T),
                Left=(int)(left*T), Right=(int)(right*T),
                HeaderDist=(int)(headerDist*T), FooterDist=(int)(footerDist*T),
                Gutter=(int)(gutter*T), MirrorMargins=mirrorMargins });
        }

        // ── Header / Footer XML ───────────────────────────────────────────────
        public byte[] GetHeaderXml(HfType type = HfType.Default)
        {
            var ptr = Native.ssDocx_get_header_xml(_handle, (int)type, out uint sz);
            return Native.PtrToBytes(ptr, sz);
        }
        public byte[] GetFooterXml(HfType type = HfType.Default)
        {
            var ptr = Native.ssDocx_get_footer_xml(_handle, (int)type, out uint sz);
            return Native.PtrToBytes(ptr, sz);
        }
        public void SetHeaderXml(HfType type, byte[] xml)
        {
            if (Native.ssDocx_set_header_xml(_handle, (int)type, xml, (uint)xml.Length) == 0)
                throw new InvalidOperationException(LastError());
        }
        public void SetFooterXml(HfType type, byte[] xml)
        {
            if (Native.ssDocx_set_footer_xml(_handle, (int)type, xml, (uint)xml.Length) == 0)
                throw new InvalidOperationException(LastError());
        }

        // ── H/F Builder (static) ──────────────────────────────────────────────
        /// <summary>
        /// Build a complete OOXML header or footer from a template string.
        /// Variables: {PAGE} {NUMPAGES} {TITLE} {CHAPTER} {AUTHOR}
        /// </summary>
        public static byte[] BuildHfXml(bool isHeader, string template,
                                         string alignment = "center")
        {
            var ptr = Native.ssDocx_build_hf_xml(isHeader ? 1 : 0,
                                                  template, alignment, out uint sz);
            var result = Native.PtrToBytes(ptr, sz);
            if (result == null) throw new InvalidOperationException("BuildHfXml failed");
            return result;
        }

        // ── Document Properties ───────────────────────────────────────────────
        public string GetDocProperty(string key) =>
            Native.PtrToStringUtf8(Native.ssDocx_get_doc_property(_handle, key));
        public void SetDocProperty(string key, string value)
        {
            if (Native.ssDocx_set_doc_property(_handle, key, value) == 0)
                throw new InvalidOperationException(LastError());
        }

        // ── Section Flags ─────────────────────────────────────────────────────
        public bool TitlePage      => Native.ssDocx_get_title_page(_handle) != 0;
        public bool EvenAndOdd     => Native.ssDocx_get_even_and_odd(_handle) != 0;
        public void SetTitlePage(bool on)   => Native.ssDocx_set_title_page(_handle, on?1:0);
        public void SetEvenAndOdd(bool on)  => Native.ssDocx_set_even_and_odd(_handle, on?1:0);
        public void SetPageNumberFormat(string fmt, int start = -1) =>
            Native.ssDocx_set_page_number_format(_handle, fmt, start);

        // ── Version ───────────────────────────────────────────────────────────
        public static string Version =>
            Native.PtrToStringUtf8(Native.ssDocx_version());

        // ── Internals ─────────────────────────────────────────────────────────
        private string LastError() =>
            Native.PtrToStringUtf8(Native.ssDocx_error(_handle));
        private static string StaticError() =>
            Native.PtrToStringUtf8(Native.ssDocx_error(IntPtr.Zero));
    }
}
