/*
 * winemsibuilder - tool to build MSI packages
 *
 * Copyright 2010 Hans Leidekker for CodeWeavers
 *
 * 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
 */

#define WIN32_LEAN_AND_MEAN
#define COBJMACROS

#include <stdio.h>
#include <windows.h>
#include <msi.h>
#include <msiquery.h>
#include <objbase.h>

#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(winemsibuilder);

static UINT open_database( const WCHAR *msifile, MSIHANDLE *handle )
{
    UINT r;
    MSIHANDLE hdb;

    if (GetFileAttributesW( msifile ) == INVALID_FILE_ATTRIBUTES)
    {
        r = MsiOpenDatabaseW( msifile, MSIDBOPEN_CREATE, &hdb );
        if (r != ERROR_SUCCESS)
        {
            WINE_ERR( "failed to create package database %s (%u)\n", wine_dbgstr_w(msifile), r );
            return r;
        }
        r = MsiDatabaseCommit( hdb );
        if (r != ERROR_SUCCESS)
        {
            WINE_ERR( "failed to commit database (%u)\n", r );
            MsiCloseHandle( hdb );
            return r;
        }
    }
    else
    {
        r = MsiOpenDatabaseW( msifile, MSIDBOPEN_TRANSACT, &hdb );
        if (r != ERROR_SUCCESS)
        {
            WINE_ERR( "failed to open package database %s (%u)\n", wine_dbgstr_w(msifile), r );
            return r;
        }
    }

    *handle = hdb;
    return ERROR_SUCCESS;
}

static int import_tables( const WCHAR *msifile, WCHAR **tables )
{
    UINT r;
    MSIHANDLE hdb;
    WCHAR *dir;
    DWORD len;

    r = open_database( msifile, &hdb );
    if (r != ERROR_SUCCESS) return 1;

    len = GetCurrentDirectoryW( 0, NULL );
    if (!(dir = HeapAlloc( GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR) )))
    {
        MsiCloseHandle( hdb );
        return 1;
    }
    GetCurrentDirectoryW( len + 1, dir );

    while (*tables)
    {
        r = MsiDatabaseImportW( hdb, dir, *tables );
        if (r != ERROR_SUCCESS)
        {
            WINE_ERR( "failed to import table %s (%u)\n", wine_dbgstr_w(*tables), r );
            break;
        }
        tables++;
    }

    if (r == ERROR_SUCCESS)
    {
        r = MsiDatabaseCommit( hdb );
        if (r != ERROR_SUCCESS)
            WINE_ERR( "failed to commit changes (%u)\n", r );
    }

    HeapFree( GetProcessHeap(), 0, dir );
    MsiCloseHandle( hdb );
    return (r != ERROR_SUCCESS);
}

/* taken from dlls/msi/table.c */
static int utf2mime( int x )
{
    if (x >= '0' && x <= '9')
        return x - '0';
    if (x >= 'A' && x <= 'Z')
        return x - 'A' + 10;
    if (x >= 'a' && x <= 'z')
        return x - 'a' + 10 + 26;
    if (x == '.')
        return 10 + 26 + 26;
    if (x == '_')
        return 10 + 26 + 26 + 1;
    return -1;
}

#define MAX_STREAM_NAME 0x1f

static WCHAR *encode_stream( const WCHAR *in )
{
    DWORD c, next, count;
    WCHAR *out, *p;

    count = lstrlenW( in );
    if (count > MAX_STREAM_NAME)
        return NULL;

    count += 2;
    if (!(out = HeapAlloc( GetProcessHeap(), 0, count * sizeof(WCHAR) ))) return NULL;
    p = out;
    while (count--)
    {
        c = *in++;
        if (!c)
        {
            *p = c;
            return out;
        }
        if (c < 0x80 && utf2mime( c ) >= 0)
        {
            c = utf2mime( c ) + 0x4800;
            next = *in;
            if (next && next < 0x80)
            {
                next = utf2mime( next );
                if (next != -1)
                {
                     next += 0x3ffffc0;
                     c += next << 6;
                     in++;
                }
            }
        }
        *p++ = c;
    }
    HeapFree( GetProcessHeap(), 0, out );
    return NULL;
}

static int add_stream( const WCHAR *msifile, const WCHAR *stream, const WCHAR *file )
{
    UINT r;
    HRESULT hr;
    MSIHANDLE hdb;
    IStorage *stg;
    IStream *stm = NULL;
    HANDLE handle;
    char buffer[4096];
    ULARGE_INTEGER size;
    DWORD low, high, read;
    WCHAR *encname;
    int ret = 1;

    /* make sure we have the right type of file  */
    r = open_database( msifile, &hdb );
    if (r != ERROR_SUCCESS) return 1;
    MsiCloseHandle( hdb );

    hr = StgOpenStorage( msifile, NULL, STGM_TRANSACTED|STGM_READWRITE|STGM_SHARE_EXCLUSIVE, NULL, 0, &stg );
    if (hr != S_OK)
    {
        WINE_WARN( "failed to open storage %s (0x%08x)\n", wine_dbgstr_w(msifile), hr );
        return 1;
    }
    encname = encode_stream( stream );
    if (!encname)
    {
        WINE_WARN( "failed to encode stream name %s\n", wine_dbgstr_w(stream) );
        goto done;
    }
    hr = IStorage_CreateStream( stg, encname, STGM_CREATE|STGM_WRITE|STGM_SHARE_EXCLUSIVE, 0, 0, &stm );
    if (hr != S_OK)
    {
        WINE_WARN( "failed to create stream %s (0x%08x)\n", wine_dbgstr_w(encname), hr );
        goto done;
    }
    handle = CreateFileW( file, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL );
    if (handle == INVALID_HANDLE_VALUE)
    {
        WINE_WARN( "failed to open file %s (%u)\n", wine_dbgstr_w(file), GetLastError() );
        goto done;
    }
    low = GetFileSize( handle, &high );
    if (low == INVALID_FILE_SIZE || high)
    {
        WINE_WARN( "file %s too big\n", wine_dbgstr_w(file) );
        CloseHandle( handle );
        goto done;
    }
    size.QuadPart = low;
    hr = IStream_SetSize( stm, size );
    if (hr != S_OK) goto done;

    while (ReadFile( handle, buffer, sizeof(buffer), &read, NULL ) && read)
    {
        hr = IStream_Write( stm, buffer, read, NULL );
        if (hr != S_OK) break;
        size.QuadPart -= read;
    }
    CloseHandle( handle );
    if (size.QuadPart)
    {
        WINE_WARN( "failed to write stream contents\n" );
        goto done;
    }
    IStorage_Commit( stg, 0 );
    ret = 0;

done:
    HeapFree( GetProcessHeap(), 0, encname );
    if (stm) IStream_Release( stm );
    IStorage_Release( stg );
    return ret;
}

static void show_usage( void )
{
    WINE_MESSAGE(
        "Usage: winemsibuilder [OPTION] [MSIFILE] ...\n"
        "Options:\n"
        "  -i package.msi table1.idt [table2.idt ...]    Import one or more tables into the database.\n"
        "  -a package.msi stream file                    Add 'stream' to storage with contents of 'file'.\n"
        "\nExisting tables or streams will be overwritten. If package.msi does not exist a new file\n"
        "will be created with an empty database.\n"
    );
}

int __cdecl wmain( int argc, WCHAR *argv[] )
{
    if (argc < 3 || argv[1][0] != '-')
    {
        show_usage();
        return 1;
    }

    switch (argv[1][1])
    {
    case 'i':
        if (argc < 4) break;
        return import_tables( argv[2], argv + 3 );
    case 'a':
        if (argc < 5) break;
        return add_stream( argv[2], argv[3], argv[4] );
    default:
        WINE_WARN( "unknown option\n" );
        break;
    }

    show_usage();
    return 1;
}