// Copyright 2019 Bloomberg Finance L.P
//
// 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.

#ifndef INCLUDED_BUILDBOXCOMMON_MERGEUTIL
#define INCLUDED_BUILDBOXCOMMON_MERGEUTIL

#include <buildboxcommon_casclient.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_protos.h>

#include <google/protobuf/repeated_field.h>

#include <vector>

namespace buildboxcommon {

class MergeFailure : public std::runtime_error {
  public:
    explicit MergeFailure(const std::string &message)
        : std::runtime_error(message)
    {
    }
};

/**
 * Example usage:
 *    buildboxcommon::Client client;
 *
 *    // assuming we've been give both digests
 *    // get the tree's for both the input and template digests;
 *    DirectoryTree inputTree, templateTree;
 *    try {
 *        inputTree = client.getTree(inputDigest);
 *        templateTree = client.getTree(templateDigest);
 *    }
 *    catch (const std::runtime_error &e) {
 *        // handle exception...
 *    }
 *
 *    // merge the two trees
 *    Digest mergedRootDigest;
 *    digest_string_map dsMap;
 *    const bool result = MergeUtil::createMergedDigest(
 *        inputTree, templateTree,
 *        &mergedRootDigest, &dsMap);
 *
 *    // save the newly generated digests to CAS
 *    grpc::ClientContext context;
 *    BatchUpdateBlobsRequest updateRequest;
 *    BatchUpdateBlobsResponse updateResponse;
 *    for (const auto &it : dsMap) {
 *        auto *request = updateRequest.add_requests();
 *        request->mutable_digest()->CopyFrom(it.first);
 *        request->mutable_data()->assign(it.second);
 *    }
 *    casClient->BatchUpdateBlobs(&context, updateRequest, &updateResponse);
 */

struct MergeUtil {
    typedef std::vector<Digest> DigestVector;
    typedef std::map<Digest, Directory> DigestDirectoryMap;

    /**
     * Create a merged Directory tree made up of the sum of all the
     * parts of the 2 input Directory's. Save the merged root digest
     * to 'rootDigest'. Save all the digests created
     * into 'newDirectoryBlobs'. Save all the newly created directory
     * digests (from merging directories) into 'mergedDirectoryList'.
     * 'mergedDirectoryList' defaults to nullptr to maintain backwards
     * compatability.
     *
     * Returns true on success, false if collisions were detected
     */
    static bool
    createMergedDigest(const DirectoryTree &inputTree,
                       const DirectoryTree &templateTree, Digest *rootDigest,
                       digest_string_map *newDirectoryBlobs,
                       DigestVector *mergedDirectoryList = nullptr);

    /**
     * Create a merged Directory tree made up of the sum of all the
     * parts of the input Directory's. Save the merged root digest
     * to 'rootDigest'. Save all the digests created
     * into 'newDirectoryBlobs'. Save all the newly created directory
     * digests (from merging directories) into 'mergedDirectoryList'.
     * 'mergedDirectoryList' defaults to nullptr to maintain backwards
     * compatability.
     *
     * Returns true on success, false if collisions were detected
     */
    static bool
    createMergedDigest(const std::vector<DirectoryTree> &treesToMerge,
                       Digest *rootDigest,
                       digest_string_map *newDirectoryBlobs,
                       DigestVector *mergedDirectoryList = nullptr);

    /**
     * Create a merged Directory tree made up of the sum of all the
     * parts of the input Directory's representing OCI filesystem layers. Save
     * the merged root digest to 'rootDigest'. Save all the digests created
     * into 'newDirectoryBlobs'.
     *
     * Returns true on success, false if all input trees are empty.
     */
    static bool
    createMergedLayersDigest(const std::vector<DirectoryTree> &treesToMerge,
                             Digest *rootDigest,
                             digest_string_map *newDirectoryBlobs);
    /**
     * Created a Digest of a merged Directory message made up of the sum of 2
     * Digest's pointing to OCI filesystem layers. All Directory's referenced
     * by the input Digest's should be available in 'digestDirectoryMap' map.
     * Save newly created Directory's into 'digestDirectoryMap' and
     * 'newDirectoriesBlobs'
     */
    static Digest mergeLayers(const Digest &prev, const Digest &next,
                              DigestDirectoryMap &digestDirectoryMap,
                              digest_string_map &newDirectoriesBlobs);

    static inline Digest emptyDirDigest()
    {
        return DigestGenerator::hash(s_emptyDir);
    };

    static inline const Directory s_emptyDir = Directory();
};

// convenience streaming operators
std::ostream &operator<<(std::ostream &out, const DirectoryTree &tree);

std::string
toString(const ::google::protobuf::RepeatedPtrField<buildboxcommon::Directory>
             &tree);

} // namespace buildboxcommon

#endif
