/*
 * 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;

import static org.apache.jackrabbit.oak.segment.DefaultSegmentWriterBuilder.defaultSegmentWriterBuilder;
import static org.apache.jackrabbit.oak.segment.file.Reclaimers.newOldReclaimer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;

import org.apache.jackrabbit.guava.common.io.Closer;
import org.apache.jackrabbit.guava.common.util.concurrent.UncheckedExecutionException;
import org.apache.jackrabbit.oak.segment.RecordId;
import org.apache.jackrabbit.oak.segment.Segment;
import org.apache.jackrabbit.oak.segment.SegmentId;
import org.apache.jackrabbit.oak.segment.SegmentWriter;
import org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions;
import org.apache.jackrabbit.oak.segment.file.tar.TarFiles;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A read only {@link AbstractFileStore} implementation that supports going back
 * to old revisions.
 * <p>
 * All write methods are no-ops.
 */
public class ReadOnlyFileStore extends AbstractFileStore {

    private static final Logger log = LoggerFactory
            .getLogger(ReadOnlyFileStore.class);

    private final TarFiles tarFiles;

    @NotNull
    private final SegmentWriter writer;
    private final int gcRetainedGenerations;

    private ReadOnlyRevisions revisions;

    private RecordId currentHead;

    ReadOnlyFileStore(FileStoreBuilder builder) throws InvalidFileStoreVersionException, IOException {
        super(builder);

        newManifestChecker(builder.getPersistence(), builder.getStrictVersionCheck()).checkManifest();

        tarFiles = TarFiles.builder()
                .withDirectory(directory)
                .withTarRecovery(recovery)
                .withIOMonitor(ioMonitor)
                .withRemoteStoreMonitor(remoteStoreMonitor)
                .withMemoryMapping(memoryMapping)
                .withReadOnly()
                .withPersistence(builder.getPersistence())
                .withInitialisedReadersAndWriters(false)
                .build();
        tarFiles.init();
        writer = defaultSegmentWriterBuilder("read-only").withoutCache().build(this);
        gcRetainedGenerations = builder.getGcOptions().getRetainedGenerations();

        log.info("TarMK ReadOnly opened: {} (mmap={})", directory,
                memoryMapping);
    }

    ReadOnlyFileStore bind(@NotNull ReadOnlyRevisions revisions) throws IOException {
        this.revisions = revisions;
        this.revisions.bind(this, tracker);
        currentHead = revisions.getHead();
        return this;
    }

    /**
     * Go to the specified {@code revision}
     *
     * @param revision
     */
    public void setRevision(String revision) {
        RecordId newHead = RecordId.fromString(tracker, revision);
        if (revisions.setHead(currentHead, newHead)) {
            currentHead = newHead;
        }
    }

    @Override
    public void writeSegment(SegmentId id, byte[] data, int offset, int length) {
        throw new UnsupportedOperationException("Read Only Store");
    }

    @Override
    public boolean containsSegment(SegmentId id) {
        return tarFiles.containsSegment(id.getMostSignificantBits(), id.getLeastSignificantBits());
    }

    @Override
    @NotNull
    public Segment readSegment(final SegmentId id) {
        try {
            return segmentCache.getSegment(id, new Callable<Segment>() {
                @Override
                public Segment call() throws Exception {
                    return readSegmentUncached(tarFiles, id);
                }
            });
        } catch (ExecutionException | UncheckedExecutionException e) {
            throw asSegmentNotFoundException(e, id);
        }
    }

    @Override
    public void close() {
        Closer closer = Closer.create();
        closer.register(tarFiles);
        closer.register(revisions);
        closeAndLogOnFail(closer);
        System.gc(); // for any memory-mappings that are no longer used
        log.info("TarMK closed: {}", directory);
    }

    @NotNull
    @Override
    public SegmentWriter getWriter() {
        return writer;
    }

    public Map<String, Set<UUID>> getTarReaderIndex() {
        return tarFiles.getIndices();
    }

    public Map<UUID, Set<UUID>> getTarGraph(String fileName) throws IOException {
        return tarFiles.getGraph(fileName);
    }

    public Iterable<SegmentId> getSegmentIds() {
        List<SegmentId> ids = new ArrayList<>();
        for (UUID id : tarFiles.getSegmentIds()) {
            long msb = id.getMostSignificantBits();
            long lsb = id.getLeastSignificantBits();
            ids.add(tracker.newSegmentId(msb, lsb));
        }
        return ids;
    }

    @Override
    public ReadOnlyRevisions getRevisions() {
        return revisions;
    }

    public Set<SegmentId> getReferencedSegmentIds() {
        return tracker.getReferencedSegmentIds();
    }

    @Override
    public void collectBlobReferences(Consumer<String> collector) throws IOException {
        tarFiles.collectBlobReferences(collector,
            newOldReclaimer(SegmentGCOptions.GCType.FULL, revisions.getHead().getSegmentId().getGcGeneration(), gcRetainedGenerations));
    }
}
