/*
 * Queue Manager (BITS) File
 *
 * Copyright 2007, 2008 Google (Roy Shea, Dan Hipschman)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include <stdarg.h>

#define COBJMACROS

#include "windef.h"
#include "winbase.h"
#include "winuser.h"
#include "winreg.h"
#include "ole2.h"
#include "urlmon.h"
#include "wininet.h"

#include "qmgr.h"
#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(qmgr);

static void BackgroundCopyFileDestructor(BackgroundCopyFileImpl *This)
{
    IBackgroundCopyJob_Release((IBackgroundCopyJob *) This->owner);
    HeapFree(GetProcessHeap(), 0, This->info.LocalName);
    HeapFree(GetProcessHeap(), 0, This->info.RemoteName);
    HeapFree(GetProcessHeap(), 0, This);
}

static ULONG WINAPI BITS_IBackgroundCopyFile_AddRef(IBackgroundCopyFile* iface)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;
    return InterlockedIncrement(&This->ref);
}

static HRESULT WINAPI BITS_IBackgroundCopyFile_QueryInterface(
    IBackgroundCopyFile* iface,
    REFIID riid,
    void **ppvObject)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;

    if (IsEqualGUID(riid, &IID_IUnknown)
        || IsEqualGUID(riid, &IID_IBackgroundCopyFile))
    {
        *ppvObject = &This->lpVtbl;
        BITS_IBackgroundCopyFile_AddRef(iface);
        return S_OK;
    }

    *ppvObject = NULL;
    return E_NOINTERFACE;
}


static ULONG WINAPI BITS_IBackgroundCopyFile_Release(
    IBackgroundCopyFile* iface)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;
    ULONG ref = InterlockedDecrement(&This->ref);

    if (ref == 0)
        BackgroundCopyFileDestructor(This);

    return ref;
}

/* Get the remote name of a background copy file */
static HRESULT WINAPI BITS_IBackgroundCopyFile_GetRemoteName(
    IBackgroundCopyFile* iface,
    LPWSTR *pVal)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;
    int n = (lstrlenW(This->info.RemoteName) + 1) * sizeof(WCHAR);

    *pVal = CoTaskMemAlloc(n);
    if (!*pVal)
        return E_OUTOFMEMORY;

    memcpy(*pVal, This->info.RemoteName, n);
    return S_OK;
}

static HRESULT WINAPI BITS_IBackgroundCopyFile_GetLocalName(
    IBackgroundCopyFile* iface,
    LPWSTR *pVal)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;
    int n = (lstrlenW(This->info.LocalName) + 1) * sizeof(WCHAR);

    *pVal = CoTaskMemAlloc(n);
    if (!*pVal)
        return E_OUTOFMEMORY;

    memcpy(*pVal, This->info.LocalName, n);
    return S_OK;
}

static HRESULT WINAPI BITS_IBackgroundCopyFile_GetProgress(
    IBackgroundCopyFile* iface,
    BG_FILE_PROGRESS *pVal)
{
    BackgroundCopyFileImpl *This = (BackgroundCopyFileImpl *) iface;

    EnterCriticalSection(&This->owner->cs);
    pVal->BytesTotal = This->fileProgress.BytesTotal;
    pVal->BytesTransferred = This->fileProgress.BytesTransferred;
    pVal->Completed = This->fileProgress.Completed;
    LeaveCriticalSection(&This->owner->cs);

    return S_OK;
}

static const IBackgroundCopyFileVtbl BITS_IBackgroundCopyFile_Vtbl =
{
    BITS_IBackgroundCopyFile_QueryInterface,
    BITS_IBackgroundCopyFile_AddRef,
    BITS_IBackgroundCopyFile_Release,
    BITS_IBackgroundCopyFile_GetRemoteName,
    BITS_IBackgroundCopyFile_GetLocalName,
    BITS_IBackgroundCopyFile_GetProgress
};

HRESULT BackgroundCopyFileConstructor(BackgroundCopyJobImpl *owner,
                                      LPCWSTR remoteName, LPCWSTR localName,
                                      LPVOID *ppObj)
{
    BackgroundCopyFileImpl *This;
    int n;

    TRACE("(%s,%s,%p)\n", debugstr_w(remoteName),
            debugstr_w(localName), ppObj);

    This = HeapAlloc(GetProcessHeap(), 0, sizeof *This);
    if (!This)
        return E_OUTOFMEMORY;

    n = (lstrlenW(remoteName) + 1) * sizeof(WCHAR);
    This->info.RemoteName = HeapAlloc(GetProcessHeap(), 0, n);
    if (!This->info.RemoteName)
    {
        HeapFree(GetProcessHeap(), 0, This);
        return E_OUTOFMEMORY;
    }
    memcpy(This->info.RemoteName, remoteName, n);

    n = (lstrlenW(localName) + 1) * sizeof(WCHAR);
    This->info.LocalName = HeapAlloc(GetProcessHeap(), 0, n);
    if (!This->info.LocalName)
    {
        HeapFree(GetProcessHeap(), 0, This->info.RemoteName);
        HeapFree(GetProcessHeap(), 0, This);
        return E_OUTOFMEMORY;
    }
    memcpy(This->info.LocalName, localName, n);

    This->lpVtbl = &BITS_IBackgroundCopyFile_Vtbl;
    This->ref = 1;

    This->fileProgress.BytesTotal = BG_SIZE_UNKNOWN;
    This->fileProgress.BytesTransferred = 0;
    This->fileProgress.Completed = FALSE;
    This->owner = owner;
    IBackgroundCopyJob_AddRef((IBackgroundCopyJob *) owner);

    *ppObj = &This->lpVtbl;
    return S_OK;
}

static DWORD CALLBACK copyProgressCallback(LARGE_INTEGER totalSize,
                                           LARGE_INTEGER totalTransferred,
                                           LARGE_INTEGER streamSize,
                                           LARGE_INTEGER streamTransferred,
                                           DWORD streamNum,
                                           DWORD reason,
                                           HANDLE srcFile,
                                           HANDLE dstFile,
                                           LPVOID obj)
{
    BackgroundCopyFileImpl *file = obj;
    BackgroundCopyJobImpl *job = file->owner;
    ULONG64 diff;

    EnterCriticalSection(&job->cs);
    diff = (file->fileProgress.BytesTotal == BG_SIZE_UNKNOWN
            ? totalTransferred.QuadPart
            : totalTransferred.QuadPart - file->fileProgress.BytesTransferred);
    file->fileProgress.BytesTotal = totalSize.QuadPart;
    file->fileProgress.BytesTransferred = totalTransferred.QuadPart;
    job->jobProgress.BytesTransferred += diff;
    LeaveCriticalSection(&job->cs);

    return (job->state == BG_JOB_STATE_TRANSFERRING
            ? PROGRESS_CONTINUE
            : PROGRESS_CANCEL);
}

typedef struct
{
    IBindStatusCallback IBindStatusCallback_iface;
    BackgroundCopyFileImpl *file;
    LONG ref;
} DLBindStatusCallback;

static inline DLBindStatusCallback *impl_from_IBindStatusCallback(IBindStatusCallback *iface)
{
    return CONTAINING_RECORD(iface, DLBindStatusCallback, IBindStatusCallback_iface);
}

static ULONG WINAPI DLBindStatusCallback_AddRef(IBindStatusCallback *iface)
{
    DLBindStatusCallback *This = impl_from_IBindStatusCallback(iface);
    return InterlockedIncrement(&This->ref);
}

static ULONG WINAPI DLBindStatusCallback_Release(IBindStatusCallback *iface)
{
    DLBindStatusCallback *This = impl_from_IBindStatusCallback(iface);
    ULONG ref = InterlockedDecrement(&This->ref);

    if (ref == 0)
    {
        IBackgroundCopyFile_Release((IBackgroundCopyFile *) This->file);
        HeapFree(GetProcessHeap(), 0, This);
    }

    return ref;
}

static HRESULT WINAPI DLBindStatusCallback_QueryInterface(
    IBindStatusCallback *iface,
    REFIID riid,
    void **ppvObject)
{
    DLBindStatusCallback *This = impl_from_IBindStatusCallback(iface);

    if (IsEqualGUID(riid, &IID_IUnknown)
        || IsEqualGUID(riid, &IID_IBindStatusCallback))
    {
        *ppvObject = &This->IBindStatusCallback_iface;
        DLBindStatusCallback_AddRef(iface);
        return S_OK;
    }

    *ppvObject = NULL;
    return E_NOINTERFACE;
}

static HRESULT WINAPI DLBindStatusCallback_GetBindInfo(
    IBindStatusCallback *iface,
    DWORD *grfBINDF,
    BINDINFO *pbindinfo)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_GetPriority(
    IBindStatusCallback *iface,
    LONG *pnPriority)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_OnDataAvailable(
    IBindStatusCallback *iface,
    DWORD grfBSCF,
    DWORD dwSize,
    FORMATETC *pformatetc,
    STGMEDIUM *pstgmed)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_OnLowResource(
    IBindStatusCallback *iface,
    DWORD reserved)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_OnObjectAvailable(
    IBindStatusCallback *iface,
    REFIID riid,
    IUnknown *punk)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_OnProgress(
    IBindStatusCallback *iface,
    ULONG progress,
    ULONG progressMax,
    ULONG statusCode,
    LPCWSTR statusText)
{
    DLBindStatusCallback *This = impl_from_IBindStatusCallback(iface);
    BackgroundCopyFileImpl *file = This->file;
    BackgroundCopyJobImpl *job = file->owner;
    ULONG64 diff;

    EnterCriticalSection(&job->cs);
    diff = (file->fileProgress.BytesTotal == BG_SIZE_UNKNOWN
            ? progress
            : progress - file->fileProgress.BytesTransferred);
    file->fileProgress.BytesTotal = progressMax ? progressMax : BG_SIZE_UNKNOWN;
    file->fileProgress.BytesTransferred = progress;
    job->jobProgress.BytesTransferred += diff;
    LeaveCriticalSection(&job->cs);

    return S_OK;
}

static HRESULT WINAPI DLBindStatusCallback_OnStartBinding(
    IBindStatusCallback *iface,
    DWORD dwReserved,
    IBinding *pib)
{
    return E_NOTIMPL;
}

static HRESULT WINAPI DLBindStatusCallback_OnStopBinding(
    IBindStatusCallback *iface,
    HRESULT hresult,
    LPCWSTR szError)
{
    return E_NOTIMPL;
}

static const IBindStatusCallbackVtbl DLBindStatusCallback_Vtbl =
{
    DLBindStatusCallback_QueryInterface,
    DLBindStatusCallback_AddRef,
    DLBindStatusCallback_Release,
    DLBindStatusCallback_OnStartBinding,
    DLBindStatusCallback_GetPriority,
    DLBindStatusCallback_OnLowResource,
    DLBindStatusCallback_OnProgress,
    DLBindStatusCallback_OnStopBinding,
    DLBindStatusCallback_GetBindInfo,
    DLBindStatusCallback_OnDataAvailable,
    DLBindStatusCallback_OnObjectAvailable
};

static DLBindStatusCallback *DLBindStatusCallbackConstructor(
    BackgroundCopyFileImpl *file)
{
    DLBindStatusCallback *This = HeapAlloc(GetProcessHeap(), 0, sizeof *This);
    if (!This)
        return NULL;

    This->IBindStatusCallback_iface.lpVtbl = &DLBindStatusCallback_Vtbl;
    IBackgroundCopyFile_AddRef((IBackgroundCopyFile *) file);
    This->file = file;
    This->ref = 1;
    return This;
}

BOOL processFile(BackgroundCopyFileImpl *file, BackgroundCopyJobImpl *job)
{
    static const WCHAR prefix[] = {'B','I','T', 0};
    DLBindStatusCallback *callbackObj;
    WCHAR tmpDir[MAX_PATH];
    WCHAR tmpName[MAX_PATH];
    HRESULT hr;

    if (!GetTempPathW(MAX_PATH, tmpDir))
    {
        ERR("Couldn't create temp file name: %d\n", GetLastError());
        /* Guessing on what state this should give us */
        transitionJobState(job, BG_JOB_STATE_QUEUED, BG_JOB_STATE_TRANSIENT_ERROR);
        return FALSE;
    }

    if (!GetTempFileNameW(tmpDir, prefix, 0, tmpName))
    {
        ERR("Couldn't create temp file: %d\n", GetLastError());
        /* Guessing on what state this should give us */
        transitionJobState(job, BG_JOB_STATE_QUEUED, BG_JOB_STATE_TRANSIENT_ERROR);
        return FALSE;
    }

    callbackObj = DLBindStatusCallbackConstructor(file);
    if (!callbackObj)
    {
        ERR("Out of memory\n");
        transitionJobState(job, BG_JOB_STATE_QUEUED, BG_JOB_STATE_TRANSIENT_ERROR);
        return FALSE;
    }

    EnterCriticalSection(&job->cs);
    file->fileProgress.BytesTotal = BG_SIZE_UNKNOWN;
    file->fileProgress.BytesTransferred = 0;
    file->fileProgress.Completed = FALSE;
    LeaveCriticalSection(&job->cs);

    TRACE("Transferring: %s -> %s -> %s\n",
          debugstr_w(file->info.RemoteName),
          debugstr_w(tmpName),
          debugstr_w(file->info.LocalName));

    transitionJobState(job, BG_JOB_STATE_QUEUED, BG_JOB_STATE_TRANSFERRING);

    DeleteUrlCacheEntryW(file->info.RemoteName);
    hr = URLDownloadToFileW(NULL, file->info.RemoteName, tmpName, 0,
                            &callbackObj->IBindStatusCallback_iface);
    IBindStatusCallback_Release(&callbackObj->IBindStatusCallback_iface);
    if (hr == INET_E_DOWNLOAD_FAILURE)
    {
        TRACE("URLDownload failed, trying local file copy\n");
        if (!CopyFileExW(file->info.RemoteName, tmpName, copyProgressCallback,
                         file, NULL, 0))
        {
            ERR("Local file copy failed: error %d\n", GetLastError());
            transitionJobState(job, BG_JOB_STATE_TRANSFERRING, BG_JOB_STATE_ERROR);
            return FALSE;
        }
    }
    else if (FAILED(hr))
    {
        ERR("URLDownload failed: eh 0x%08x\n", hr);
        transitionJobState(job, BG_JOB_STATE_TRANSFERRING, BG_JOB_STATE_ERROR);
        return FALSE;
    }

    if (transitionJobState(job, BG_JOB_STATE_TRANSFERRING, BG_JOB_STATE_QUEUED))
    {
        lstrcpyW(file->tempFileName, tmpName);

        EnterCriticalSection(&job->cs);
        file->fileProgress.Completed = TRUE;
        job->jobProgress.FilesTransferred++;
        LeaveCriticalSection(&job->cs);

        return TRUE;
    }
    else
    {
        DeleteFileW(tmpName);
        return FALSE;
    }
}