import os
import tempfile

import pythoncom
import win32api
import win32event
from win32com.bits import bits
from win32com.server.util import wrap

TIMEOUT = 200  # ms
StopEvent = win32event.CreateEvent(None, 0, 0, None)

job_name = "bits-pywin32-test"
states = {
    val: (name[13:])
    for name, val in vars(bits).items()
    if name.startswith("BG_JOB_STATE_")
}

bcm = pythoncom.CoCreateInstance(
    bits.CLSID_BackgroundCopyManager,
    None,
    pythoncom.CLSCTX_LOCAL_SERVER,
    bits.IID_IBackgroundCopyManager,
)


class BackgroundJobCallback:
    _com_interfaces_ = [bits.IID_IBackgroundCopyCallback]
    _public_methods_ = ["JobTransferred", "JobError", "JobModification"]

    def JobTransferred(self, job):
        print("Job Transferred", job)
        job.Complete()
        win32event.SetEvent(StopEvent)  # exit msg pump

    def JobError(self, job, error):
        print("Job Error", job, error)
        f = error.GetFile()
        print("While downloading", f.GetRemoteName())
        print("To", f.GetLocalName())
        print("The following error happened:")
        self._print_error(error)
        if f.GetRemoteName().endswith("missing-favicon.ico"):
            print("Changing to point to correct file")
            f2 = f.QueryInterface(bits.IID_IBackgroundCopyFile2)
            favicon = "https://www.python.org/favicon.ico"
            print("Changing RemoteName from", f2.GetRemoteName(), "to", favicon)
            f2.SetRemoteName(favicon)
            job.Resume()
        else:
            job.Cancel()

    def _print_error(self, err):
        ctx, hresult = err.GetError()
        try:
            hresult_msg = win32api.FormatMessage(hresult)
        except win32api.error:
            hresult_msg = ""
        print(f"Context=0x{ctx:x}, hresult=0x{hresult:x} ({hresult_msg})")
        print(err.GetErrorDescription())

    def JobModification(self, job, reserved):
        state = job.GetState()
        print("Job Modification", job.GetDisplayName(), states.get(state))
        # Need to catch TRANSIENT_ERROR here, as JobError doesn't get
        # called (apparently) when the error is transient.
        if state == bits.BG_JOB_STATE_TRANSIENT_ERROR:
            print("Error details:")
            err = job.GetError()
            self._print_error(err)


job = bcm.CreateJob(job_name, bits.BG_JOB_TYPE_DOWNLOAD)

job.SetNotifyInterface(wrap(BackgroundJobCallback()))
job.SetNotifyFlags(
    bits.BG_NOTIFY_JOB_TRANSFERRED
    | bits.BG_NOTIFY_JOB_ERROR
    | bits.BG_NOTIFY_JOB_MODIFICATION
)


# The idea here is to intentionally make one of the files fail to be
# downloaded. Then the JobError notification will be triggered, where
# we do fix the failing file by calling SetRemoteName to a valid URL
# and call Resume() on the job, making the job finish successfully.
#
# Note to self: A domain that cannot be resolved will cause
# TRANSIENT_ERROR instead of ERROR, and the JobError notification will
# not be triggered! This can bite you during testing depending on how
# your DNS is configured. For example, if you use OpenDNS.org's DNS
# servers, an invalid hostname will *always* be resolved (they
# redirect you to a search page), so be careful when testing.
job.AddFile(
    "https://www.python.org/favicon.ico",
    os.path.join(tempfile.gettempdir(), "bits-favicon.ico"),
)
job.AddFile(
    "https://www.python.org/missing-favicon.ico",
    os.path.join(tempfile.gettempdir(), "bits-missing-favicon.ico"),
)

for f in job.EnumFiles():
    print("Downloading", f.GetRemoteName())
    print("To", f.GetLocalName())

job.Resume()

while True:
    rc = win32event.MsgWaitForMultipleObjects(
        (StopEvent,), 0, TIMEOUT, win32event.QS_ALLEVENTS
    )

    if rc == win32event.WAIT_OBJECT_0:
        break
    elif rc == win32event.WAIT_OBJECT_0 + 1:
        if pythoncom.PumpWaitingMessages():
            break  # wm_quit
