/*
 * Copyright 2018 Nikolay Sivov 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 COBJMACROS

#include <stdarg.h>
#include <zlib.h>

#include "windef.h"
#include "winternl.h"
#include "msopc.h"

#include "opc_private.h"

#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(msopc);

#include <pshpack2.h>
struct local_file_header
{
    DWORD signature;
    WORD version;
    WORD flags;
    WORD method;
    DWORD mtime;
    DWORD crc32;
    DWORD compressed_size;
    DWORD uncompressed_size;
    WORD name_length;
    WORD extra_length;
};

struct data_descriptor
{
    DWORD signature;
    DWORD crc32;
    DWORD compressed_size;
    DWORD uncompressed_size;
};

struct central_directory_header
{
    DWORD signature;
    WORD version;
    WORD min_version;
    WORD flags;
    WORD method;
    DWORD mtime;
    DWORD crc32;
    DWORD compressed_size;
    DWORD uncompressed_size;
    WORD name_length;
    WORD extra_length;
    WORD comment_length;
    WORD diskid;
    WORD internal_attributes;
    DWORD external_attributes;
    DWORD local_file_offset;
};

struct central_directory_end
{
    DWORD signature;
    WORD diskid;
    WORD firstdisk;
    WORD records_num;
    WORD records_total;
    DWORD directory_size;
    DWORD directory_offset;
    WORD comment_length;
};
#include <poppack.h>

#define CENTRAL_DIR_SIGNATURE 0x02014b50
#define LOCAL_HEADER_SIGNATURE 0x04034b50
#define DIRECTORY_END_SIGNATURE 0x06054b50
#define DATA_DESCRIPTOR_SIGNATURE 0x08074b50
#define VERSION 20

enum entry_flags
{
    USE_DATA_DESCRIPTOR = 0x8,
};

struct zip_archive
{
    struct central_directory_header **files;
    size_t file_count;
    size_t file_size;

    DWORD mtime;
    IStream *output;
    DWORD position;
    HRESULT write_result;

    unsigned char input_buffer[0x8000];
    unsigned char output_buffer[0x8000];
};

HRESULT compress_create_archive(IStream *output, struct zip_archive **out)
{
    struct zip_archive *archive;
    WORD date, time;
    FILETIME ft;

    if (!(archive = malloc(sizeof(*archive))))
        return E_OUTOFMEMORY;

    archive->files = NULL;
    archive->file_size = 0;
    archive->file_count = 0;
    archive->write_result = S_OK;

    archive->output = output;
    IStream_AddRef(archive->output);
    archive->position = 0;

    GetSystemTimeAsFileTime(&ft);
    FileTimeToDosDateTime(&ft, &date, &time);
    archive->mtime = date << 16 | time;

    *out = archive;

    return S_OK;
}

static void compress_write(struct zip_archive *archive, void *data, ULONG size)
{
    ULONG written;

    archive->write_result = IStream_Write(archive->output, data, size, &written);
    if (written != size)
        archive->write_result = E_FAIL;
    else
        archive->position += written;

    if (FAILED(archive->write_result))
        WARN("Failed to write output %p, size %lu, written %lu, hr %#lx.\n",
                data, size, written, archive->write_result);
}

void compress_finalize_archive(struct zip_archive *archive)
{
    struct central_directory_end dir_end = { 0 };
    size_t i;

    dir_end.directory_offset = archive->position;
    dir_end.records_num = archive->file_count;
    dir_end.records_total = archive->file_count;

    /* Directory entries */
    for (i = 0; i < archive->file_count; ++i)
    {
        compress_write(archive, archive->files[i], sizeof(*archive->files[i]));
        compress_write(archive, archive->files[i] + 1, archive->files[i]->name_length);
        dir_end.directory_size += archive->files[i]->name_length + sizeof(*archive->files[i]);
    }

    /* End record */
    dir_end.signature = DIRECTORY_END_SIGNATURE;
    compress_write(archive, &dir_end, sizeof(dir_end));

    IStream_Release(archive->output);

    for (i = 0; i < archive->file_count; i++)
        free(archive->files[i]);
    free(archive->files);
    free(archive);
}

static void compress_write_content(struct zip_archive *archive, IStream *content,
        OPC_COMPRESSION_OPTIONS options, struct data_descriptor *data_desc)
{
    int level, flush;
    z_stream z_str;
    LARGE_INTEGER move;
    ULONG num_read;
    HRESULT hr;

    data_desc->crc32 = RtlComputeCrc32(0, NULL, 0);
    move.QuadPart = 0;
    IStream_Seek(content, move, STREAM_SEEK_SET, NULL);

    switch (options)
    {
    case OPC_COMPRESSION_NONE:
        level = Z_NO_COMPRESSION;
        break;
    case OPC_COMPRESSION_NORMAL:
        level = Z_DEFAULT_COMPRESSION;
        break;
    case OPC_COMPRESSION_MAXIMUM:
        level = Z_BEST_COMPRESSION;
        break;
    case OPC_COMPRESSION_FAST:
        level = 2;
        break;
    case OPC_COMPRESSION_SUPERFAST:
        level = Z_BEST_SPEED;
        break;
    default:
        WARN("Unsupported compression options %d.\n", options);
        level = Z_DEFAULT_COMPRESSION;
    }

    memset(&z_str, 0, sizeof(z_str));
    deflateInit2(&z_str, level, Z_DEFLATED, -MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY);

    do
    {
        int ret;

        if (FAILED(hr = IStream_Read(content, archive->input_buffer, sizeof(archive->input_buffer), &num_read)))
        {
            archive->write_result = hr;
            break;
        }

        z_str.avail_in = num_read;
        z_str.next_in = archive->input_buffer;
        data_desc->crc32 = RtlComputeCrc32(data_desc->crc32, archive->input_buffer, num_read);

        flush = sizeof(archive->input_buffer) > num_read ? Z_FINISH : Z_NO_FLUSH;

        do
        {
            ULONG have;

            z_str.avail_out = sizeof(archive->output_buffer);
            z_str.next_out = archive->output_buffer;

            if ((ret = deflate(&z_str, flush)))
                WARN("Failed to deflate, ret %d.\n", ret);
            have = sizeof(archive->output_buffer) - z_str.avail_out;
            compress_write(archive, archive->output_buffer, have);
        } while (z_str.avail_out == 0);
    } while (flush != Z_FINISH);

    deflateEnd(&z_str);

    data_desc->compressed_size = z_str.total_out;
    data_desc->uncompressed_size = z_str.total_in;
}

HRESULT compress_add_file(struct zip_archive *archive, const WCHAR *path,
        IStream *content, OPC_COMPRESSION_OPTIONS options)
{
    struct central_directory_header *entry;
    struct local_file_header local_header;
    struct data_descriptor data_desc;
    DWORD local_header_pos;
    char *name;
    DWORD len;

    len = WideCharToMultiByte(CP_ACP, 0, path, -1, NULL, 0, NULL, NULL);
    if (!(name = malloc(len)))
        return E_OUTOFMEMORY;
    WideCharToMultiByte(CP_ACP, 0, path, -1, name, len, NULL, NULL);

    /* Local header */
    local_header.signature = LOCAL_HEADER_SIGNATURE;
    local_header.version = VERSION;
    local_header.flags = USE_DATA_DESCRIPTOR;
    local_header.method = 8; /* Z_DEFLATED */
    local_header.mtime = archive->mtime;
    local_header.crc32 = 0;
    local_header.compressed_size = 0;
    local_header.uncompressed_size = 0;
    local_header.name_length = len - 1;
    local_header.extra_length = 0;

    local_header_pos = archive->position;

    compress_write(archive, &local_header, sizeof(local_header));
    compress_write(archive, name, local_header.name_length);

    /* Content */
    compress_write_content(archive, content, options, &data_desc);

    /* Data descriptor */
    data_desc.signature = DATA_DESCRIPTOR_SIGNATURE;
    compress_write(archive, &data_desc, sizeof(data_desc));

    if (FAILED(archive->write_result))
        return archive->write_result;

    /* Set directory entry */
    if (!(entry = calloc(1, sizeof(*entry) + local_header.name_length)))
    {
        free(name);
        return E_OUTOFMEMORY;
    }

    entry->signature = CENTRAL_DIR_SIGNATURE;
    entry->version = local_header.version;
    entry->min_version = local_header.version;
    entry->flags = local_header.flags;
    entry->method = local_header.method;
    entry->mtime = local_header.mtime;
    entry->crc32 = data_desc.crc32;
    entry->compressed_size = data_desc.compressed_size;
    entry->uncompressed_size = data_desc.uncompressed_size;
    entry->name_length = local_header.name_length;
    entry->local_file_offset = local_header_pos;
    memcpy(entry + 1, name, entry->name_length);
    free(name);

    if (!opc_array_reserve((void **)&archive->files, &archive->file_size, archive->file_count + 1,
            sizeof(*archive->files)))
    {
        free(entry);
        return E_OUTOFMEMORY;
    }

    archive->files[archive->file_count++] = entry;

    return S_OK;
}