#include "stdinc.h"
#include "st.h"
#include "msiinstall.h"

CDeque<MSIINSTALLTEST_THREAD_PROC_DATA, offsetof(MSIINSTALLTEST_THREAD_PROC_DATA, Linkage)> g_MSIInstallTest;

#define DATA_DIRECTORY_NAME             L"msiinstall"
#define SLASH_DATA_DIRECTORY_SLASH_STAR L"\\" DATA_DIRECTORY_NAME L"\\*"
#define SLASH_DATA_DIRECTORY_SLASH      L"\\" DATA_DIRECTORY_NAME L"\\"
#define INI_FILE                        L"msiinstall.ini"
#define SLASH_INI_FILE                  L"\\msiinstall.ini"

BOOL InitializeMSIInstallTest()
{
    BOOL fSuccess = FALSE;
    FN_TRACE_WIN32(fSuccess);

    CFindFile hFind;
    WIN32_FIND_DATAW wfd;
    CStringBuffer TempDirectory;
    CDequeIterator<MSIINSTALLTEST_THREAD_PROC_DATA, offsetof(MSIINSTALLTEST_THREAD_PROC_DATA, Linkage)> iter(&g_MSIInstallTest);
    CStringBuffer IniFilePath;
    WCHAR buf[MAX_PATH];
    DWORD bufSize = NUMBER_OF(buf);
    WCHAR FileKey[10]; // at most, there are 10^10 files in the assembly, including manifest
    DWORD rSize;
    STRINGBUFFER_LINKAGE * pTinyStringBuffer = NULL;
    MSIINSTALLTEST_THREAD_PROC_DATA *pData = NULL;


    if (!TempDirectory.Win32Assign(BaseDirectory))
        goto Exit;

    if (!TempDirectory.Win32AppendPathElement(DATA_DIRECTORY_NAME, NUMBER_OF(DATA_DIRECTORY_NAME) - 1))
        goto Exit;

    if ((wfd.dwFileAttributes = ::GetFileAttributesW(TempDirectory)) == 0xffffffff
        && (wfd.dwFileAttributes = ::FusionpGetLastWin32Error()) == ERROR_FILE_NOT_FOUND)
    {
        printf("no %ls tests, skipping\n", DATA_DIRECTORY_NAME);
        FN_SUCCESSFUL_EXIT();
    }

    if (!TempDirectory.Win32AppendPathElement(L"*", 1))
        goto Exit;

    hFind = ::FindFirstFileW(TempDirectory, &wfd);
    if (hFind == INVALID_HANDLE_VALUE)
    {
        ::ReportFailure("Failed to find any files matching \"%ls\"\n", static_cast<PCWSTR>(TempDirectory));
        goto Exit;
    }
    
    for (;;)
    {
        CSmallStringBuffer TempString;
        CSmallStringBuffer BaseDirectory2;

        if (FusionpIsDotOrDotDot(wfd.cFileName))
              goto Skip;

        if (!BaseDirectory2.Win32Assign(BaseDirectory))
            goto Exit;

        if (!BaseDirectory2.Win32AppendPathElement(DATA_DIRECTORY_NAME, NUMBER_OF(DATA_DIRECTORY_NAME) - 1))
            goto Exit;

        if (!BaseDirectory2.Win32AppendPathElement(wfd.cFileName, wcslen(wfd.cFileName)))
            goto Exit;

        if ((pData = new MSIINSTALLTEST_THREAD_PROC_DATA) == NULL)
        {
            ::FusionpSetLastWin32Error(ERROR_OUTOFMEMORY);
            ::ReportFailure("Failed to allocate MSIINSTALLTEST_THREAD_PROC_DATA\n");
            goto Exit;
        }

        if (!IniFilePath.Win32Assign(BaseDirectory2))
            goto Exit;
        if (!IniFilePath.Win32AppendPathElement(INI_FILE, NUMBER_OF(INI_FILE) - 1))
            goto Exit;

        if (GetFileAttributesW(IniFilePath) == DWORD(-1))
        {
            ::ReportFailure("Failed to find msiinstall.ini from %s.\n", IniFilePath);
            goto Exit;
        }

        // 
        // get manifest filename
        //
        rSize = GetPrivateProfileStringW(L"general", L"manifest", L"", buf, bufSize, IniFilePath);
        if ((rSize == bufSize - 1) || (rSize == 0))
        {
            ::ReportFailure("manifest filename in %s is erroneous, either too long or empty\n", IniFilePath);
            goto Exit;
        }
        IFW32FALSE_EXIT(pData->ManifestFileName.Win32Assign(buf, rSize));

        // 
        // get AssemblyNamefromDarwin
        //
        rSize = GetPrivateProfileStringW(L"general", L"AssemblyNameFromDarwin", L"", buf, bufSize, IniFilePath);
        if ((rSize == bufSize - 1) || (rSize == 0))
        {
            ::ReportFailure("assemblyname from darwin in %s is erroneous, either too long or empty\n", IniFilePath);
            goto Exit;
        }
        IFW32FALSE_EXIT(pData->AssemblyNameFromDarwin.Win32Assign(buf, rSize));

        //
        // get the SourceFile directory from ini or use the current directory
        //
        rSize = GetPrivateProfileStringW(L"general", L"AssemblySourceFileDirectory", L"", buf, bufSize, IniFilePath);
        if (rSize == bufSize - 1)
            goto Exit;
        if (rSize != 0)
            IFW32FALSE_EXIT(pData->AssemblySourceDirectory.Win32Assign(buf, rSize));
        else
            IFW32FALSE_EXIT(pData->AssemblySourceDirectory.Win32Assign(BaseDirectory2));

        for (DWORD i=0;;i++)
        {
            swprintf(FileKey, L"%1d", i); // make FileKey in WSTR
            rSize = GetPrivateProfileStringW(L"files", FileKey, L"", buf, bufSize, IniFilePath);
            if (rSize == bufSize - 1)// the value string in .ini is too long 
                goto Exit;
            if ( rSize == 0 ) // get all
                break;

            pTinyStringBuffer = new STRINGBUFFER_LINKAGE;
            if (pTinyStringBuffer  == NULL)
            {
                ::FusionpSetLastWin32Error(ERROR_INSUFFICIENT_BUFFER);
                goto Exit;
            }
                
            IFW32FALSE_EXIT(pTinyStringBuffer->Str.Win32Assign(buf, rSize));
            pData->FileNameOfAssemblyList.AddToTail(pTinyStringBuffer);
            pTinyStringBuffer = NULL;
        }
        
        g_MSIInstallTest.AddToTail(pData);
        pData = NULL;

Skip:
        if (!::FindNextFileW(hFind, &wfd))
        {
            if (::FusionpGetLastWin32Error() != ERROR_NO_MORE_FILES)
            {
                ::ReportFailure("Error iterating over assemblies\n");
                goto Exit;
            }
            break;
        }
    }

    for (iter.Reset(); iter.More(); iter.Next())
    {
        if (!iter->Thread.Win32CreateThread(&MSIInstallTestThreadProc, iter.Current()))
        {
            ::ReportFailure("Error launching install thread\n");
            goto Exit;
        }
        TotalThreads += 1;
    }

    fSuccess = TRUE;
Exit:
    if (pData)
        delete pData;

    if (pTinyStringBuffer != NULL)
        delete pTinyStringBuffer;

    return fSuccess;
}

void RequestShutdownMSIInstallTestThreads()
{
    CDequeIterator<MSIINSTALLTEST_THREAD_PROC_DATA, offsetof(MSIINSTALLTEST_THREAD_PROC_DATA, Linkage)> iter(&g_MSIInstallTest);

    for (iter.Reset(); iter.More(); iter.Next())
    {
        iter->Stop = true;
    }

}
void WaitForMSIInstallTestThreads()
{
    CDequeIterator<MSIINSTALLTEST_THREAD_PROC_DATA, offsetof(MSIINSTALLTEST_THREAD_PROC_DATA, Linkage)> iter(&g_MSIInstallTest);

    for (iter.Reset(); iter.More(); iter.Next())
    {
        DWORD WaitResult = ::WaitForSingleObject(iter->Thread, INFINITE);
        switch (WaitResult)
        {
        case WAIT_OBJECT_0:
            break;
        case WAIT_FAILED:
            ::ReportFailure("MSIInstall :: Failed to WaitForSingleObject.\n");
            break;
        default:
            ::FusionpSetLastWin32Error(WaitResult);
            ::ReportFailure("MSIInstall :: Failed to WaitForSingleObject.\n");
            break;
        }
        iter->Thread.Win32Close();
    }
}

void CleanupMSIInstallTest()
{
    g_MSIInstallTest.ClearAndDeleteAll();
}

HRESULT Helper_WriteStream(CSmallStringBuffer * pFileNameBuf,
                           IStream *pStream)
{
    HRESULT     hr          = NOERROR;
    LPBYTE      pBuf[0x4000];
    DWORD       cbBuf       = 0x4000;
    DWORD       dwWritten   = 0;
    DWORD       cbRead      = 0;
    HANDLE      hf          = INVALID_HANDLE_VALUE;

    hf = ::CreateFileW(static_cast<PCWSTR>(*pFileNameBuf), GENERIC_READ, FILE_SHARE_READ,
            NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
    if (hf == INVALID_HANDLE_VALUE){
        hr = HRESULT_FROM_WIN32 (GetLastError());
        goto Exit;
    }

    while (::ReadFile(hf, pBuf, cbBuf, &cbRead, NULL) && cbRead){
        hr = pStream->Write(pBuf, cbRead, &dwWritten);
        if (FAILED(hr))
            goto Exit;
    }

    if (! SUCCEEDED(hr = pStream->Commit(0)))
        goto Exit;

    CloseHandle(hf);

Exit:
    return hr;
}

DWORD
WINAPI
MSIInstallTestThreadProc(
    LPVOID pvData
    )
{
    DWORD dwReturnValue = ERROR_INTERNAL_ERROR;
    DWORD WaitResult = 0;
    MSIINSTALLTEST_THREAD_PROC_DATA *pData = reinterpret_cast<MSIINSTALLTEST_THREAD_PROC_DATA*>(pvData);
    IAssemblyCache * pAsmCache = NULL;
    SIZE_T nPathLen = pData->AssemblySourceDirectory.Cch();
    IAssemblyCacheItem * pCacheItem = NULL;
    IStream * pStream = NULL;
    IAssemblyName * pAssemblyName = NULL;
    WCHAR buf[MAX_PATH];
    DWORD bufSize = MAX_PATH;
    
    
    HRESULT hr;

    InterlockedIncrement(&ThreadsWaiting);
    WaitResult = WaitForSingleObject(ResumeThreadsEvent, INFINITE);
    switch (WaitResult)
    {
    case WAIT_OBJECT_0:
        break;
    case WAIT_FAILED:
        dwReturnValue = ::FusionpGetLastWin32Error();
        ::ReportFailure("Failed to WaitForSingleObject.\n");
        goto Exit;
    default:
        dwReturnValue = WaitResult;
        ::FusionpSetLastWin32Error(WaitResult);
        ::ReportFailure("Failed to WaitForSingleObject.\n");
        goto Exit;
    }


    if (CreateAssemblyCache(&pAsmCache, 0) != S_OK)
    {
        ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateAssemblyCache with gle %lu \n",
                    SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                    pData->AssemblySourceDirectory,
                    pData->ManifestFileName,
                    ::FusionpGetLastWin32Error());
        goto Exit; 
    }

    while (!pData->Stop)
    {
        //Create AssemblyCache and AssemblyCacheItem        
        if ((hr = pAsmCache->CreateAssemblyCacheItem(0, NULL, &pCacheItem, NULL)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateAssemblyCacheItem with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }

        //
        // create manifests for assembly item
        //
        if ((hr = pCacheItem->CreateStream(0, pData->ManifestFileName, STREAM_FORMAT_WIN32_MANIFEST, 0, &pStream, NULL)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateStream for manifest with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        ::FusionpGetLastWin32Error());
            goto Exit; 

        }
        pData->AssemblySourceDirectory.Left(nPathLen);

        if (! pData->AssemblySourceDirectory.Win32AppendPathElement(pData->ManifestFileName))
        {
            goto Exit;
        }

        if ( (hr = Helper_WriteStream(&pData->AssemblySourceDirectory, pStream)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed WriteStream for manifest with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }

        pStream->Release();

        CDequeIterator<STRINGBUFFER_LINKAGE, offsetof(STRINGBUFFER_LINKAGE, Linkage)> iter(&pData->FileNameOfAssemblyList);
        for (iter.Reset(); iter.More(); iter.Next())
        {
            if ((hr = pCacheItem->CreateStream(0, iter.Current()->Str, STREAM_FORMAT_WIN32_MODULE, 0, &pStream, NULL)) != S_OK)
            {
                ::FusionpSetLastErrorFromHRESULT(hr);
                ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateStream for module \"%ls\" with gle %lu \n",
                            SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                            pData->AssemblySourceDirectory,
                            pData->ManifestFileName,
                            iter->Str,
                            ::FusionpGetLastWin32Error());
                goto Exit; 
            }
            pData->AssemblySourceDirectory.Left(nPathLen);
            if (! pData->AssemblySourceDirectory.Win32AppendPathElement(iter.Current()->Str))
                goto Exit;
            if ( (hr = Helper_WriteStream(&pData->AssemblySourceDirectory, pStream)) != S_OK)
            {
                ::FusionpSetLastErrorFromHRESULT(hr);
                ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateStream for module \"%ls\" with gle %lu \n",
                            SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                            pData->AssemblySourceDirectory,
                            pData->ManifestFileName,
                            iter->Str,
                            ::FusionpGetLastWin32Error());
                goto Exit; 

            }
            pStream->Release();
        }

        if ((hr = pCacheItem->Commit(0, NULL)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed Commit AssemblyCacheItem with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }


        // uninstall the same assembly using CAssemblyName and CAssemblyCache->UninstallAssembly

        if ( (hr = CreateAssemblyNameObject(&pAssemblyName, pData->AssemblyNameFromDarwin, CANOF_PARSE_DISPLAY_NAME,NULL)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed CreateAssemblyName for %s with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        pData->AssemblyNameFromDarwin,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }

        if ( (hr = pAssemblyName->GetDisplayName(buf, &bufSize, 0)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) failed IsAssemblyInstalled for %s with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        pData->AssemblyNameFromDarwin,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }

        if ( wcscmp(pData->AssemblyNameFromDarwin, buf) != 0)
        {
            goto Exit;
        }


        if ((hr = pAsmCache->UninstallAssembly(0, buf, NULL, NULL)) != S_OK)
        {
            ::FusionpSetLastErrorFromHRESULT(hr);
            ::ReportFailure("[%lx.%lx] MSIInstall(\"%ls\", 0x%lx) Cache->UninstallAssemblyfailed for %s with gle %lu \n",
                        SxStressToolGetCurrentProcessId(), SxStressToolGetCurrentThreadId(),
                        pData->AssemblySourceDirectory,
                        pData->ManifestFileName,
                        buf,
                        ::FusionpGetLastWin32Error());
            goto Exit; 
        }

        ::WaitForSingleObject(StopEvent, pData->Sleep);
    }
    
    dwReturnValue = ERROR_SUCCESS;
    goto Cleanup;
Exit:
    dwReturnValue = ::FusionpGetLastWin32Error();    

Cleanup: 
    if (pAsmCache)
        pAsmCache->Release();

    if (pCacheItem)
        pCacheItem->Release();

    if (pStream)
        pStream->Release();

    if (pAssemblyName)
        pAssemblyName->Release();   
    
    return dwReturnValue;
}