/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * 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 "pkg_gzipfile.h"
#include "dump.h"

using namespace std;

namespace Hpackage {
/* gzip flag byte */
constexpr uint16_t HEADER_CRC = 0x02; /* bit 1 set: CRC16 for the gzip header */
constexpr uint16_t EXTRA_FIELD = 0x04; /* bit 2 set: extra field present */
constexpr uint16_t ORIG_NAME = 0x08; /* bit 3 set: original file name present */
constexpr uint16_t COMMENT = 0x10; /* bit 4 set: file comment present */
constexpr uint16_t ENCRYPTED = 0x20; /* bit 5 set: file is encrypted */
constexpr int32_t DEF_MEM_LEVEL = 8;
constexpr uint16_t GZIP_MAGIC = 0x8b1f;
constexpr int32_t BUFFER_SIZE = 1024;
#ifdef SUPPORT_EXTRA_FIELD
constexpr int32_t EXTRA_FIELD_LEN = 20;
#endif
constexpr int32_t BLOCK_SIZE = 8;

/*
    Each member has the following structure:
     +---+---+---+---+---+---+---+---+---+---+
     |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
     +---+---+---+---+---+---+---+---+---+---+

    (if FLG.FEXTRA set)

     +---+---+=================================+
     | XLEN  |...XLEN bytes of "extra field"...| (more-->)
     +---+---+=================================+

    (if FLG.FNAME set)

     +=========================================+
     |...original file name, zero-terminated...| (more-->)
     +=========================================+

    (if FLG.FCOMMENT set)

     +===================================+
     |...file comment, zero-terminated...| (more-->)
     +===================================+

    (if FLG.FHCRC set)

     +---+---+
     | CRC16 |
     +---+---+
 */
void GZipFileEntry::GetUpGradeCompInfo(size_t &offset, PkgBuffer &buffer)
{
    GZipHeader *header = (GZipHeader *)buffer.buffer;
    header->magic = GZIP_MAGIC;
    header->method = Z_DEFLATED;
    header->flags = 0;
    header->mtime = fileInfo_.fileInfo.modifiedTime;
    offset += sizeof(GZipHeader);
#ifdef SUPPORT_EXTRA_FIELD
    header->flags |= EXTRA_FIELD;
    {
        WriteLE16(buffer.buffer + offset, EXTRA_FIELD_LEN);
        offset += sizeof(uint16_t) + EXTRA_FIELD_LEN;
    }
#endif
    header->flags |= ORIG_NAME;
    {
        size_t fileNameLen = 0;
        PkgFileImpl::ConvertStringToBuffer(
            fileInfo_.fileInfo.identity, {buffer.buffer + offset, buffer.length - offset}, fileNameLen);
        offset += fileNameLen;
        buffer.buffer[offset] = 0;
        offset += 1;
    }
#ifdef SUPPORT_EXTRA_FIELD
    header->flags |= COMMENT;
    {
        size_t fileNameLen = 0;
        PkgFileImpl::ConvertStringToBuffer(
            fileInfo_.fileInfo.identity, {buffer.buffer + offset, buffer.length - offset}, fileNameLen);
        offset += fileNameLen;
        buffer.buffer[offset] = 0;
        offset += 1;
    }
#endif
    return ;
}

int32_t GZipFileEntry::EncodeHeader(PkgStreamPtr inStream, size_t startOffset, size_t &encodeLen)
{
    PkgStreamPtr outStream = pkgFile_->GetPkgStream();
    if (outStream == nullptr) {
        PKG_LOGE("Check outstream fail %s", fileInfo_.fileInfo.identity.c_str());
        return PKG_INVALID_PARAM;
    }
    size_t offset = 0;
    PkgBuffer buffer(BUFFER_SIZE);

    GetUpGradeCompInfo(offset, buffer);

    fileInfo_.fileInfo.headerOffset = startOffset;
    fileInfo_.fileInfo.dataOffset = startOffset + offset;
    int32_t ret = outStream->Write(buffer, offset, startOffset);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail write header for %s", fileInfo_.fileInfo.identity.c_str());
        return ret;
    }
    encodeLen = offset;
    return PKG_SUCCESS;
}

int32_t GZipFileEntry::Pack(PkgStreamPtr inStream, size_t startOffset, size_t &encodeLen)
{
    PkgAlgorithm::PkgAlgorithmPtr algorithm = PkgAlgorithmFactory::GetAlgorithm(&fileInfo_.fileInfo);
    PkgStreamPtr outStream = pkgFile_->GetPkgStream();
    if (fileInfo_.fileInfo.dataOffset != startOffset) {
        PKG_LOGE("start offset error for %s", fileInfo_.fileInfo.identity.c_str());
        return PKG_INVALID_PARAM;
    }
    if (algorithm == nullptr || outStream == nullptr || inStream == nullptr) {
        PKG_LOGE("outStream or inStream null for %s", fileInfo_.fileInfo.identity.c_str());
        return PKG_INVALID_PARAM;
    }
    fileInfo_.fileInfo.dataOffset = startOffset;
    PkgAlgorithmContext context = {
        {0, startOffset},
        {fileInfo_.fileInfo.packedSize, fileInfo_.fileInfo.unpackedSize},
        0, fileInfo_.fileInfo.digestMethod
    };
    int32_t ret = algorithm->Pack(inStream, outStream, context);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail Compress for %s", fileInfo_.fileInfo.identity.c_str());
        return ret;
    }
    fileInfo_.fileInfo.packedSize = context.packedSize;

    /*
    0   1   2   3   4   5   6   7
    +---+---+---+---+---+---+---+---+
    |     CRC32     |     ISIZE     |
    +---+---+---+---+---+---+---+---+
    */
    PkgBuffer buffer(BLOCK_SIZE);
    WriteLE32(buffer.buffer, context.crc);
    WriteLE32(buffer.buffer + sizeof(uint32_t), fileInfo_.fileInfo.unpackedSize);
    ret = outStream->Write(buffer, BLOCK_SIZE, fileInfo_.fileInfo.dataOffset + fileInfo_.fileInfo.packedSize);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail write header for %s", fileInfo_.fileInfo.identity.c_str());
        return ret;
    }
    encodeLen = fileInfo_.fileInfo.packedSize + BLOCK_SIZE;
    PKG_LOGI("Pack packedSize:%zu unpackedSize: %zu offset: %zu %zu", fileInfo_.fileInfo.packedSize,
        fileInfo_.fileInfo.unpackedSize, fileInfo_.fileInfo.headerOffset, fileInfo_.fileInfo.dataOffset);
    return PKG_SUCCESS;
}

int32_t GZipFileEntry::CheckFileInfo(PkgAlgorithmContext context, PkgStreamPtr inStream)
{
    size_t readLen = 0;
    fileInfo_.fileInfo.packedSize = context.packedSize;
    PkgBuffer buffer(BLOCK_SIZE); // Read last 8 bytes at the end of package
    int32_t ret = inStream->Read(buffer, context.packedSize + fileInfo_.fileInfo.dataOffset, BLOCK_SIZE, readLen);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail to read file %s", inStream->GetFileName().c_str());
        return ret;
    }
    crc32_ = ReadLE32(buffer.buffer);
    fileInfo_.fileInfo.unpackedSize = ReadLE32(buffer.buffer + sizeof(uint32_t));
    if (crc32_ != context.crc) {
        PKG_LOGE("Crc error %u %u", crc32_, context.crc);
        return PKG_VERIFY_FAIL;
    }

    if (fileInfo_.fileInfo.unpackedSize != context.unpackedSize) {
        PKG_LOGE("Crc error %u %u", crc32_, context.crc);
        return PKG_VERIFY_FAIL;
    }
    return PKG_SUCCESS;
}

int32_t GZipFileEntry::Unpack(PkgStreamPtr outStream)
{
    PkgAlgorithm::PkgAlgorithmPtr algorithm = PkgAlgorithmFactory::GetAlgorithm(&fileInfo_.fileInfo);
    if (algorithm == nullptr) {
        PKG_LOGE("can not algorithm for %s", fileInfo_.fileInfo.identity.c_str());
        return PKG_INVALID_PARAM;
    }

    PKG_LOGI("packedSize: %zu unpackedSize: %zu  offset header: %zu data: %zu", fileInfo_.fileInfo.packedSize,
        fileInfo_.fileInfo.unpackedSize, fileInfo_.fileInfo.headerOffset, fileInfo_.fileInfo.dataOffset);

    PkgStreamPtr inStream = pkgFile_->GetPkgStream();
    if (outStream == nullptr || inStream == nullptr) {
        PKG_LOGE("outStream or inStream null for %s", fileInfo_.fileInfo.identity.c_str());
        return PKG_INVALID_PARAM;
    }

    PkgAlgorithmContext context = {
        {fileInfo_.fileInfo.dataOffset, 0},
        {fileInfo_.fileInfo.packedSize, fileInfo_.fileInfo.unpackedSize},
        0, fileInfo_.fileInfo.digestMethod
    };
    int32_t ret = algorithm->Unpack(inStream, outStream, context);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail Decompress for %s", fileInfo_.fileInfo.identity.c_str());
        return ret;
    }

    ret = CheckFileInfo(context, inStream);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("unpack failed ret is %d", ret);
        return ret;
    }

    PKG_LOGI("packedSize: %zu unpackedSize: %zu  offset header: %zu data: %zu", fileInfo_.fileInfo.packedSize,
        fileInfo_.fileInfo.unpackedSize, fileInfo_.fileInfo.headerOffset, fileInfo_.fileInfo.dataOffset);
    outStream->Flush(fileInfo_.fileInfo.unpackedSize);
    algorithm->UpdateFileInfo(&fileInfo_.fileInfo);
    return PKG_SUCCESS;
}

void GZipFileEntry::DecodeHeaderCalOffset(uint8_t flags, const PkgBuffer &buffer, size_t &offset,
    std::string &fileName) const
{
    if (flags & EXTRA_FIELD) {
        uint16_t extLen = ReadLE16(buffer.buffer + offset);
        offset += sizeof(uint16_t) + extLen;
    }
    if (flags & ORIG_NAME) {
        PkgFileImpl::ConvertBufferToString(fileName, {buffer.buffer + offset, buffer.length - offset});
        offset += fileName.size() + 1;
    }
    if (flags & COMMENT) {
        std::string comment;
        PkgFileImpl::ConvertBufferToString(comment, {buffer.buffer + offset, buffer.length - offset});
        offset += comment.size() + 1;
    }
    if (flags & HEADER_CRC) { // Skip CRC
        offset += sizeof(uint16_t);
    }
    return;
}

int32_t GZipFileEntry::DecodeHeader(PkgBuffer &buffer, size_t headerOffset, size_t dataOffset,
    size_t &decodeLen)
{
    Updater::UPDATER_INIT_RECORD;
    PkgStreamPtr inStream = pkgFile_->GetPkgStream();
    if (inStream == nullptr || buffer.buffer == nullptr) {
        PKG_LOGE("outStream or inStream null for %s", fileInfo_.fileInfo.identity.c_str());
        UPDATER_LAST_WORD(PKG_INVALID_PARAM);
        return PKG_INVALID_PARAM;
    }
    size_t offset = sizeof(GZipHeader);

    uint8_t flags = *(buffer.buffer + offsetof(GZipHeader, flags));

    DecodeHeaderCalOffset(flags, buffer, offset, fileName_);
    if (fileName_.empty()) {
        fileInfo_.fileInfo.identity = "gzip_";
        fileInfo_.fileInfo.identity.append(std::to_string(nodeId_));
        fileName_ = fileInfo_.fileInfo.identity;
    } else {
        fileInfo_.fileInfo.identity = fileName_;
    }
    fileInfo_.fileInfo.digestMethod = PKG_DIGEST_TYPE_CRC;
    fileInfo_.fileInfo.packMethod = PKG_COMPRESS_METHOD_GZIP;
    fileInfo_.level = Z_BEST_COMPRESSION;
    fileInfo_.method = Z_DEFLATED;
    fileInfo_.windowBits = -MAX_WBITS;
    fileInfo_.memLevel = DEF_MEM_LEVEL;
    fileInfo_.strategy = Z_DEFAULT_STRATEGY;

    fileInfo_.fileInfo.headerOffset = headerOffset;
    fileInfo_.fileInfo.dataOffset = headerOffset + offset;
    // Read data length here.
    // The length read here maybe incorrect, so should adjust it
    // when unpack.
    size_t readLen = 0;
    size_t blockOffset = inStream->GetFileLength() - BLOCK_SIZE;
    int32_t ret = inStream->Read(buffer, blockOffset, buffer.length, readLen);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail to read file %s", inStream->GetFileName().c_str());
        UPDATER_LAST_WORD(ret);
        return ret;
    }
    fileInfo_.fileInfo.unpackedSize = ReadLE32(buffer.buffer + sizeof(uint32_t));
    fileInfo_.fileInfo.packedSize = blockOffset - fileInfo_.fileInfo.dataOffset;
    PKG_LOGI("GZipFileEntry::DecodeHeader dataOffset %zu, packedSize: %zu %zu", fileInfo_.fileInfo.dataOffset,
        fileInfo_.fileInfo.packedSize, fileInfo_.fileInfo.unpackedSize);
    decodeLen = offset;
    return PKG_SUCCESS;
}

int32_t GZipPkgFile::AddEntry(const PkgManager::FileInfoPtr file, const PkgStreamPtr inStream)
{
    if (file == nullptr || inStream == nullptr) {
        PKG_LOGE("Fail to check input param");
        return PKG_INVALID_PARAM;
    }
    if (!CheckState({PKG_FILE_STATE_IDLE, PKG_FILE_STATE_WORKING}, PKG_FILE_STATE_CLOSE)) {
        PKG_LOGE("error state curr %d ", state_);
        return PKG_INVALID_STATE;
    }
    PKG_LOGI("Add file %s to package", file->identity.c_str());

    GZipFileEntry *entry = static_cast<GZipFileEntry *>(AddPkgEntry(file->identity));
    if (entry == nullptr) {
        PKG_LOGE("Fail create pkg node for %s", file->identity.c_str());
        return PKG_NONE_MEMORY;
    }
    int32_t ret = entry->Init(file, inStream);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail init entry for %s", file->identity.c_str());
        return ret;
    }

    size_t encodeLen = 0;
    ret = entry->EncodeHeader(inStream, currentOffset_, encodeLen);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail encode header for %s", file->identity.c_str());
        return ret;
    }
    currentOffset_ += encodeLen;
    ret = entry->Pack(inStream, currentOffset_, encodeLen);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail Pack for %s", file->identity.c_str());
        return ret;
    }
    currentOffset_ += encodeLen;
    pkgStream_->Flush(currentOffset_);
    return PKG_SUCCESS;
}

int32_t GZipPkgFile::SavePackage(size_t &offset)
{
    AddSignData(pkgInfo_.digestMethod, currentOffset_, offset);
    return PKG_SUCCESS;
}

int32_t GZipPkgFile::LoadPackage(std::vector<std::string> &fileNames, VerifyFunction verifier)
{
    UNUSED(verifier);
    if (!CheckState({ PKG_FILE_STATE_IDLE }, PKG_FILE_STATE_WORKING)) {
        PKG_LOGE("error state curr %d ", state_);
        UPDATER_LAST_WORD(PKG_INVALID_STATE);
        return PKG_INVALID_STATE;
    }
    PKG_LOGI("LoadPackage %s ", pkgStream_->GetFileName().c_str());
    size_t srcOffset = 0;
    size_t readLen = 0;
    PkgBuffer buffer(nullptr, BUFFER_SIZE);
    int32_t ret = pkgStream_->Read(buffer, srcOffset, buffer.length, readLen);
    if (ret != PKG_SUCCESS) {
        PKG_LOGE("Fail to read file %s", pkgStream_->GetFileName().c_str());
        UPDATER_LAST_WORD(ret);
        return ret;
    }

    GZipHeader *header = (GZipHeader *)buffer.buffer;
    // Check magic number
    if (header->magic != GZIP_MAGIC) {
        PKG_LOGE("Invalid gzip file %s", pkgStream_->GetFileName().c_str());
        UPDATER_LAST_WORD(PKG_INVALID_STATE);
        return PKG_INVALID_FILE;
    }
    // Does not support encryption
    if ((header->flags & ENCRYPTED) != 0) {
        PKG_LOGE("Not support encrypted ");
        UPDATER_LAST_WORD(PKG_INVALID_STATE);
        return PKG_INVALID_FILE;
    }

    GZipFileEntry *entry = new GZipFileEntry(this, nodeId_++);
    if (entry == nullptr) {
        PKG_LOGE("Fail create gzip node for %s", pkgStream_->GetFileName().c_str());
        UPDATER_LAST_WORD(PKG_INVALID_STATE);
        return PKG_LZ4_FINISH;
    }
    ret = entry->DecodeHeader(buffer, srcOffset, srcOffset, readLen);
    srcOffset += readLen;

    // Save entry
    pkgEntryMapId_.insert(std::pair<uint32_t, PkgEntryPtr>(entry->GetNodeId(), entry));
    pkgEntryMapFileName_.insert(std::pair<std::string, PkgEntryPtr>(entry->GetFileName(), entry));
    fileNames.push_back(entry->GetFileName());
    return ret;
}
} // namespace Hpackage
