"""
IEdmFile13::ChangeState3 via comtypes vtable direct call.

Confirmed from gen_py IEdmFile13_vtables_:
  - IEdmFile13 IID : {DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}
  - ChangeState3 oVft = 432  →  vtable slot 54
  - Slot layout:
      0-2   IUnknown  (handled by comtypes base)
      3-6   IDispatch (handled by comtypes base)
      7-53  47 methods from IEdmObject5 … IEdmFile12  (placeholders)
      54    ChangeState3
      55    GetThumbnail
"""
import ctypes
import getpass
import pythoncom
import win32com.client
import win32com.client.gencache as gencache
import comtypes
import comtypes.automation
from comtypes import COMMETHOD, GUID, HRESULT
from comtypes.automation import IDispatch as CT_IDispatch

VAULT_NAME   = "Drilling_Test"
TEST_PATH    = r"C:\PDM\Drilling_Test\DWS\PileDRIVER\825 PileDRIVER\MFG\800-TT-001.SLDPRT"
FOLDER_PATH  = r"C:\PDM\Drilling_Test\DWS\PileDRIVER\825 PileDRIVER\MFG"
TRANSITION_ID = 268
TO_STATE_ID   = 9
EdmObject_File = 1
IID_IEdmFile13 = "{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}"

# ---------------------------------------------------------------------------
# comtypes interface — 47 placeholders put ChangeState3 at slot 54 (offset 432)
# ---------------------------------------------------------------------------
_phs = [COMMETHOD([], HRESULT, f"_ph{i}") for i in range(47)]

VARIANT_p = ctypes.POINTER(comtypes.automation.VARIANT)

class IEdmFile13_CT(CT_IDispatch):
    _iid_      = GUID(IID_IEdmFile13)
    _idlflags_ = ["dual", "oleautomation"]
    _methods_  = _phs + [
        COMMETHOD(
            [], HRESULT, "ChangeState3",
            (["in"], VARIANT_p,        "poStateIdOrName"),
            (["in"], VARIANT_p,        "poTransitionIdOrName"),
            (["in"], ctypes.c_long,    "lFolderID"),
            (["in"], ctypes.c_wchar_p, "bsComment"),
            (["in"], ctypes.c_long,    "lParentWnd"),
            (["in"], ctypes.c_long,    "lEdmStateFlags"),
            (["in"], ctypes.c_wchar_p, "bsPasswd"),
        ),
        COMMETHOD(
            [], HRESULT, "GetThumbnail",
            (["out", "retval"], ctypes.POINTER(ctypes.c_void_p), "pBitmap"),
        ),
    ]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_i4_variant(val: int) -> comtypes.automation.VARIANT:
    """Return a 16-byte VARIANT with vt=VT_I4 and the given integer value."""
    v = comtypes.automation.VARIANT()
    v.vt = 3  # VT_I4
    # The union value starts at byte offset 8 inside VARIANT
    ctypes.cast(ctypes.byref(v, 8), ctypes.POINTER(ctypes.c_int))[0] = val
    return v


def raw_ptr_from_pycom(py_com_obj) -> int:
    """
    Read the IUnknown*/IDispatch* stored inside a pythoncom COM wrapper.
    CPython 64-bit layout: ob_refcnt(8) | ob_type(8) | punk(8)  → offset 16.
    """
    return ctypes.c_uint64.from_address(id(py_com_obj) + 16).value


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    username = input("PDM Username: ")
    password = getpass.getpass("PDM Password: ")  # reused for bsPasswd below

    print("\n=== Connect ===")
    vault = gencache.EnsureDispatch("ConisioLib.EdmVault")
    vault.Login(username, password, VAULT_NAME)
    print(f"Logged in to {VAULT_NAME}")

    folder_obj = vault.GetFolderFromPath(FOLDER_PATH)
    result     = vault.GetFileFromPath(TEST_PATH, folder_obj)
    raw        = result[0] if isinstance(result, tuple) else result
    file_id    = raw.ID
    fold_id    = folder_obj.ID
    print(f"File ID: {file_id}  Folder ID: {fold_id}")

    file_obj  = vault.GetObject(EdmObject_File, file_id)
    file13_w32 = win32com.client.CastTo(file_obj, "IEdmFile13")

    # -----------------------------------------------------------------------
    print("\n=== Extract raw IEdmFile13* from pythoncom ===")
    # -----------------------------------------------------------------------
    py_disp  = file13_w32._oleobj_          # PyIDispatch wrapping IEdmFile13
    raw_ptr  = raw_ptr_from_pycom(py_disp)
    print(f"IEdmFile13* = {raw_ptr:#x}")

    # Sanity: read the vtable pointer (first 8 bytes of the COM object)
    vtbl_ptr = ctypes.c_uint64.from_address(raw_ptr).value
    print(f"Vtable ptr  = {vtbl_ptr:#x}")

    if not raw_ptr or not vtbl_ptr:
        print("ERROR: Could not read a valid COM pointer — aborting.")
        return

    # -----------------------------------------------------------------------
    print("\n=== QI to IEdmFile13_CT via comtypes ===")
    # -----------------------------------------------------------------------
    # Cast to IUnknown so comtypes can call QueryInterface properly
    ct_unk = ctypes.cast(raw_ptr, ctypes.POINTER(comtypes.IUnknown))
    # py_disp must stay alive while ct_unk is in use (ct_unk is a borrowed ref)
    try:
        file13_ct = ct_unk.QueryInterface(IEdmFile13_CT)
        print(f"QI succeeded: {file13_ct}")
    except Exception as e:
        print(f"QI failed: {e}")
        import traceback; traceback.print_exc()
        return

    # -----------------------------------------------------------------------
    print("\n=== Build VARIANTs ===")
    # -----------------------------------------------------------------------
    v_state = make_i4_variant(TO_STATE_ID)
    v_trans = make_i4_variant(TRANSITION_ID)
    print(f"v_state  vt={v_state.vt}  val={TO_STATE_ID}")
    print(f"v_trans  vt={v_trans.vt}  val={TRANSITION_ID}")

    # -----------------------------------------------------------------------
    print(f"\n=== ChangeState3(state={TO_STATE_ID}, trans={TRANSITION_ID}, folder={fold_id}) ===")
    # -----------------------------------------------------------------------
    try:
        hr = file13_ct.ChangeState3(
            ctypes.byref(v_state),
            ctypes.byref(v_trans),
            ctypes.c_long(fold_id),
            "Batch transition test",
            ctypes.c_long(0),
            ctypes.c_long(0),
            password,          # PDM password (required by this transition)
        )
        print(f"ChangeState3 returned HRESULT {hr:#010x}")
    except Exception as e:
        print(f"ChangeState3 raised: {e}")
        import traceback; traceback.print_exc()
        # Keep py_disp alive
        del py_disp
        return

    # keep py_disp alive until after the call
    del py_disp

    # -----------------------------------------------------------------------
    print("\n=== Verify state ===")
    # -----------------------------------------------------------------------
    fresh = vault.GetObject(EdmObject_File, file_id)
    try:
        state = fresh.CurrentState
        if callable(state):
            state = state()
        name = state.Name if hasattr(state, "Name") else str(state)
        print(f"New state: {name}")
        if name in ("Approved", "AA"):
            print("\n*** SUCCESS! ***")
        else:
            print("State did not reach Approved.")
    except Exception as e:
        print(f"Could not read new state: {e}")


if __name__ == "__main__":
    main()
