/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

package org.apache.jackrabbit.oak.segment.file.tar.binaries;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.zip.CRC32;

import org.apache.jackrabbit.oak.commons.Buffer;

import org.apache.jackrabbit.guava.common.base.Charsets;

/**
 * Maintains the transient state of a binary references index, formats it and
 * serializes it.
 */
public class BinaryReferencesIndexWriter {

    /**
     * Create a new, empty instance of {@link BinaryReferencesIndexWriter}.
     *
     * @return An instance of {@link BinaryReferencesIndexWriter}.
     */
    public static BinaryReferencesIndexWriter newBinaryReferencesIndexWriter() {
        return new BinaryReferencesIndexWriter();
    }

    private final Map<Generation, Map<UUID, Set<String>>> entries;

    private BinaryReferencesIndexWriter() {
        entries = new HashMap<>();
    }

    /**
     * Add an entry to the binary references index.
     *
     * @param generation The generation of the segment containing the
     *                   reference.
     * @param full       The full generation of the segment containing the
     *                   reference.
     * @param compacted  {@code true} if the segment containing the reference is
     *                   created by a compaction operation.
     * @param segment    The identifier of the segment containing the
     *                   reference.
     * @param reference  The binary reference.
     */
    public void addEntry(int generation, int full, boolean compacted, UUID segment, String reference) {
        entries
            .computeIfAbsent(new Generation(generation, full, compacted), k -> new HashMap<>())
            .computeIfAbsent(segment, k -> new HashSet<>())
            .add(reference);
    }

    /**
     * Write the current state of this instance to an array of bytes.
     *
     * @return An array of bytes containing the serialized state of the binary
     * references index.
     */
    public byte[] write() {
        int binaryReferenceSize = 0;

        // The following information are stored in the footer as meta-
        // information about the entry.

        // 4 bytes to store a magic number identifying this entry as containing
        // references to binary values.
        binaryReferenceSize += 4;

        // 4 bytes to store the CRC32 checksum of the data in this entry.
        binaryReferenceSize += 4;

        // 4 bytes to store the length of this entry, without including the
        // optional padding.
        binaryReferenceSize += 4;

        // 4 bytes to store the number of generations pairs in the binary
        // references map.
        binaryReferenceSize += 4;

        // The following information are stored as part of the main content of
        // this entry, after the optional padding.

        for (Map<UUID, Set<String>> segmentToReferences : entries.values()) {
            // 4 bytes per generation to store the generation number.
            binaryReferenceSize += 4;

            // 4 bytes per generation to store the full generation number.
            binaryReferenceSize += 4;

            // 1 byte per generation to store the "compacted" flag.
            binaryReferenceSize += 1;

            // 4 bytes per generation to store the number of segments.
            binaryReferenceSize += 4;

            for (Set<String> references : segmentToReferences.values()) {
                // 16 bytes per segment identifier.
                binaryReferenceSize += 16;

                // 4 bytes to store the number of references for this segment.
                binaryReferenceSize += 4;

                for (String reference : references) {
                    // 4 bytes for each reference to store the length of the reference.
                    binaryReferenceSize += 4;

                    // A variable amount of bytes, depending on the reference itself.
                    binaryReferenceSize += reference.getBytes(Charsets.UTF_8).length;
                }
            }
        }

        Buffer buffer = Buffer.allocate(binaryReferenceSize);

        for (Entry<Generation, Map<UUID, Set<String>>> be : entries.entrySet()) {
            Generation generation = be.getKey();
            Map<UUID, Set<String>> segmentToReferences = be.getValue();

            buffer.putInt(generation.generation);
            buffer.putInt(generation.full);
            buffer.put((byte) (generation.compacted ? 1 : 0));
            buffer.putInt(segmentToReferences.size());

            for (Entry<UUID, Set<String>> se : segmentToReferences.entrySet()) {
                UUID segmentId = se.getKey();
                Set<String> references = se.getValue();

                buffer.putLong(segmentId.getMostSignificantBits());
                buffer.putLong(segmentId.getLeastSignificantBits());
                buffer.putInt(references.size());

                for (String reference : references) {
                    byte[] bytes = reference.getBytes(Charsets.UTF_8);

                    buffer.putInt(bytes.length);
                    buffer.put(bytes);
                }
            }
        }

        CRC32 checksum = new CRC32();
        checksum.update(buffer.array(), 0, buffer.position());

        buffer.putInt((int) checksum.getValue());
        buffer.putInt(entries.size());
        buffer.putInt(binaryReferenceSize);
        buffer.putInt(BinaryReferencesIndexLoaderV2.MAGIC);

        return buffer.array();
    }

}
