/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "deploy_patch_generator.h"

#include <inttypes.h>
#include <stdio.h>

#include <algorithm>
#include <fstream>
#include <functional>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>

#include <openssl/md5.h>

#include "adb_unique_fd.h"
#include "adb_utils.h"
#include "android-base/file.h"
#include "patch_utils.h"
#include "sysdeps.h"

using namespace com::android::fastdeploy;

void DeployPatchGenerator::Log(const char* fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    vprintf(fmt, ap);
    printf("\n");
    va_end(ap);
}

static std::string HexEncode(const void* in_buffer, unsigned int size) {
    static const char kHexChars[] = "0123456789ABCDEF";

    // Each input byte creates two output hex characters.
    std::string out_buffer(size * 2, '\0');

    for (unsigned int i = 0; i < size; ++i) {
        char byte = ((const uint8_t*)in_buffer)[i];
        out_buffer[(i << 1)] = kHexChars[(byte >> 4) & 0xf];
        out_buffer[(i << 1) + 1] = kHexChars[byte & 0xf];
    }
    return out_buffer;
}

void DeployPatchGenerator::APKEntryToLog(const APKEntry& entry) {
    if (!is_verbose_) {
        return;
    }
    Log("MD5: %s", HexEncode(entry.md5().data(), entry.md5().size()).c_str());
    Log("Data Offset: %" PRId64, entry.dataoffset());
    Log("Data Size: %" PRId64, entry.datasize());
}

void DeployPatchGenerator::APKMetaDataToLog(const APKMetaData& metadata) {
    if (!is_verbose_) {
        return;
    }
    Log("APK Metadata: %s", metadata.absolute_path().c_str());
    for (int i = 0; i < metadata.entries_size(); i++) {
        const APKEntry& entry = metadata.entries(i);
        APKEntryToLog(entry);
    }
}

void DeployPatchGenerator::ReportSavings(const std::vector<SimpleEntry>& identicalEntries,
                                         uint64_t totalSize) {
    uint64_t totalEqualBytes = 0;
    uint64_t totalEqualFiles = 0;
    for (size_t i = 0; i < identicalEntries.size(); i++) {
        if (identicalEntries[i].deviceEntry != nullptr) {
            totalEqualBytes += identicalEntries[i].localEntry->datasize();
            totalEqualFiles++;
        }
    }
    double savingPercent = (totalEqualBytes * 100.0f) / totalSize;
    fprintf(stderr, "Detected %" PRIu64 " equal APK entries\n", totalEqualFiles);
    fprintf(stderr, "%" PRIu64 " bytes are equal out of %" PRIu64 " (%.2f%%)\n", totalEqualBytes,
            totalSize, savingPercent);
}

struct PatchEntry {
    int64_t deltaFromDeviceDataStart = 0;
    int64_t deviceDataOffset = 0;
    int64_t deviceDataLength = 0;
};
static void WritePatchEntry(const PatchEntry& patchEntry, borrowed_fd input, borrowed_fd output,
                            size_t* realSizeOut) {
    if (!(patchEntry.deltaFromDeviceDataStart | patchEntry.deviceDataOffset |
          patchEntry.deviceDataLength)) {
        return;
    }

    PatchUtils::WriteLong(patchEntry.deltaFromDeviceDataStart, output);
    if (patchEntry.deltaFromDeviceDataStart > 0) {
        PatchUtils::Pipe(input, output, patchEntry.deltaFromDeviceDataStart);
    }
    auto hostDataLength = patchEntry.deviceDataLength;
    adb_lseek(input, hostDataLength, SEEK_CUR);

    PatchUtils::WriteLong(patchEntry.deviceDataOffset, output);
    PatchUtils::WriteLong(patchEntry.deviceDataLength, output);

    *realSizeOut += patchEntry.deltaFromDeviceDataStart + hostDataLength;
}

void DeployPatchGenerator::GeneratePatch(const std::vector<SimpleEntry>& entriesToUseOnDevice,
                                         const std::string& localApkPath,
                                         const std::string& deviceApkPath, borrowed_fd output) {
    unique_fd input(adb_open(localApkPath.c_str(), O_RDONLY | O_CLOEXEC));
    size_t newApkSize = adb_lseek(input, 0L, SEEK_END);
    adb_lseek(input, 0L, SEEK_SET);

    // Header.
    PatchUtils::WriteSignature(output);
    PatchUtils::WriteLong(newApkSize, output);
    PatchUtils::WriteString(deviceApkPath, output);

    size_t currentSizeOut = 0;
    size_t realSizeOut = 0;
    // Write data from the host upto the first entry we have that matches a device entry. Then write
    // the metadata about the device entry and repeat for all entries that match on device. Finally
    // write out any data left. If the device and host APKs are exactly the same this ends up
    // writing out zip metadata from the local APK followed by offsets to the data to use from the
    // device APK.
    PatchEntry patchEntry;
    for (size_t i = 0, size = entriesToUseOnDevice.size(); i < size; ++i) {
        auto&& entry = entriesToUseOnDevice[i];
        int64_t hostDataOffset = entry.localEntry->dataoffset();
        int64_t hostDataLength = entry.localEntry->datasize();
        int64_t deviceDataOffset = entry.deviceEntry->dataoffset();
        // Both entries are the same, using host data length.
        int64_t deviceDataLength = hostDataLength;

        int64_t deltaFromDeviceDataStart = hostDataOffset - currentSizeOut;
        if (deltaFromDeviceDataStart > 0) {
            WritePatchEntry(patchEntry, input, output, &realSizeOut);
            patchEntry.deltaFromDeviceDataStart = deltaFromDeviceDataStart;
            patchEntry.deviceDataOffset = deviceDataOffset;
            patchEntry.deviceDataLength = deviceDataLength;
        } else {
            patchEntry.deviceDataLength += deviceDataLength;
        }

        currentSizeOut += deltaFromDeviceDataStart + hostDataLength;
    }
    WritePatchEntry(patchEntry, input, output, &realSizeOut);
    if (realSizeOut != currentSizeOut) {
        fprintf(stderr, "Size mismatch current %lld vs real %lld\n",
                static_cast<long long>(currentSizeOut), static_cast<long long>(realSizeOut));
        exit(1);
    }

    if (newApkSize > currentSizeOut) {
        PatchUtils::WriteLong(newApkSize - currentSizeOut, output);
        PatchUtils::Pipe(input, output, newApkSize - currentSizeOut);
        PatchUtils::WriteLong(0, output);
        PatchUtils::WriteLong(0, output);
    }
}

bool DeployPatchGenerator::CreatePatch(const char* localApkPath, APKMetaData deviceApkMetadata,
                                       android::base::borrowed_fd output) {
    return CreatePatch(PatchUtils::GetHostAPKMetaData(localApkPath), std::move(deviceApkMetadata),
                       output);
}

bool DeployPatchGenerator::CreatePatch(APKMetaData localApkMetadata, APKMetaData deviceApkMetadata,
                                       borrowed_fd output) {
    // Log metadata info.
    APKMetaDataToLog(deviceApkMetadata);
    APKMetaDataToLog(localApkMetadata);

    const std::string localApkPath = localApkMetadata.absolute_path();
    const std::string deviceApkPath = deviceApkMetadata.absolute_path();

    std::vector<SimpleEntry> identicalEntries;
    uint64_t totalSize =
            BuildIdenticalEntries(identicalEntries, localApkMetadata, deviceApkMetadata);
    ReportSavings(identicalEntries, totalSize);
    GeneratePatch(identicalEntries, localApkPath, deviceApkPath, output);

    return true;
}

uint64_t DeployPatchGenerator::BuildIdenticalEntries(std::vector<SimpleEntry>& outIdenticalEntries,
                                                     const APKMetaData& localApkMetadata,
                                                     const APKMetaData& deviceApkMetadata) {
    outIdenticalEntries.reserve(
            std::min(localApkMetadata.entries_size(), deviceApkMetadata.entries_size()));

    using md5Digest = std::pair<uint64_t, uint64_t>;
    struct md5Hash {
        size_t operator()(const md5Digest& digest) const {
            std::hash<uint64_t> hasher;
            size_t seed = 0;
            seed ^= hasher(digest.first) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
            seed ^= hasher(digest.second) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
            return seed;
        }
    };
    static_assert(sizeof(md5Digest) == MD5_DIGEST_LENGTH);
    std::unordered_map<md5Digest, std::vector<const APKEntry*>, md5Hash> deviceEntries;
    for (const auto& deviceEntry : deviceApkMetadata.entries()) {
        md5Digest md5;
        memcpy(static_cast<void*>(&md5), deviceEntry.md5().data(), deviceEntry.md5().size());

        deviceEntries[md5].push_back(&deviceEntry);
    }

    uint64_t totalSize = 0;
    for (const auto& localEntry : localApkMetadata.entries()) {
        totalSize += localEntry.datasize();

        md5Digest md5;
        memcpy(static_cast<void*>(&md5), localEntry.md5().data(), localEntry.md5().size());

        auto deviceEntriesIt = deviceEntries.find(md5);
        if (deviceEntriesIt == deviceEntries.end()) {
            continue;
        }

        for (const auto* deviceEntry : deviceEntriesIt->second) {
            if (deviceEntry->md5() == localEntry.md5()) {
                SimpleEntry simpleEntry;
                simpleEntry.localEntry = &localEntry;
                simpleEntry.deviceEntry = deviceEntry;
                APKEntryToLog(localEntry);
                outIdenticalEntries.push_back(simpleEntry);
                break;
            }
        }
    }
    std::sort(outIdenticalEntries.begin(), outIdenticalEntries.end(),
              [](const SimpleEntry& lhs, const SimpleEntry& rhs) {
                  return lhs.localEntry->dataoffset() < rhs.localEntry->dataoffset();
              });
    return totalSize;
}
