/*
 * 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.nifi.provenance.store;

import org.apache.lucene.util.NamedThreadFactory;
import org.apache.nifi.events.EventReporter;
import org.apache.nifi.provenance.RepositoryConfiguration;
import org.apache.nifi.provenance.index.EventIndex;
import org.apache.nifi.provenance.serialization.EventFileCompressor;
import org.apache.nifi.provenance.store.iterator.AggregateEventIterator;
import org.apache.nifi.provenance.store.iterator.EventIterator;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

public class PartitionedWriteAheadEventStore extends PartitionedEventStore {
    private final BlockingQueue<File> filesToCompress;
    private final List<WriteAheadStorePartition> partitions;
    private final RepositoryConfiguration repoConfig;

    private final ExecutorService compressionExecutor;
    private final List<EventFileCompressor> fileCompressors = Collections.synchronizedList(new ArrayList<>());
    private final EventReporter eventReporter;
    private final EventFileManager fileManager;

    public PartitionedWriteAheadEventStore(final RepositoryConfiguration repoConfig, final RecordWriterFactory recordWriterFactory,
        final RecordReaderFactory recordReaderFactory, final EventReporter eventReporter, final EventFileManager fileManager) {
        super(repoConfig, eventReporter);
        this.repoConfig = repoConfig;
        this.eventReporter = eventReporter;
        this.filesToCompress = new LinkedBlockingQueue<>(100);
        final AtomicLong idGenerator = new AtomicLong(0L);
        this.fileManager = fileManager;
        this.partitions = createPartitions(repoConfig, recordWriterFactory, recordReaderFactory, idGenerator);

        // Creates tasks to compress data on rollover
        if (repoConfig.isCompressOnRollover()) {
            compressionExecutor = Executors.newFixedThreadPool(repoConfig.getIndexThreadPoolSize(), new NamedThreadFactory("Compress Provenance Logs"));
        } else {
            compressionExecutor = null;
        }
    }

    private List<WriteAheadStorePartition> createPartitions(final RepositoryConfiguration repoConfig, final RecordWriterFactory recordWriterFactory,
        final RecordReaderFactory recordReaderFactory, final AtomicLong idGenerator) {
        final Map<String, File> storageDirectories = repoConfig.getStorageDirectories();
        final List<WriteAheadStorePartition> partitions = new ArrayList<>(storageDirectories.size());

        for (final Map.Entry<String, File> entry : storageDirectories.entrySet()) {
            // Need to ensure that the same partition directory always gets the same partition index.
            // If we don't, then we will end up re-indexing the events from 1 index into another index, and
            // this will result in a lot of duplicates (up to a million per index per restart). This is the reason
            // that we use a partition name here based on the properties file.
            final String partitionName = entry.getKey();
            final File storageDirectory = entry.getValue();
            partitions.add(new WriteAheadStorePartition(storageDirectory, partitionName, repoConfig,
                recordWriterFactory, recordReaderFactory, filesToCompress, idGenerator, eventReporter, fileManager));
        }

        return partitions;
    }

    @Override
    public void initialize() throws IOException {
        if (repoConfig.isCompressOnRollover()) {
            for (int i = 0; i < repoConfig.getIndexThreadPoolSize(); i++) {
                final EventFileCompressor compressor = new EventFileCompressor(filesToCompress, fileManager);
                compressionExecutor.submit(compressor);
                fileCompressors.add(compressor);
            }
        }

        super.initialize();
    }

    @Override
    public void close() throws IOException {
        super.close();

        for (final EventFileCompressor compressor : fileCompressors) {
            compressor.shutdown();
        }

        if (compressionExecutor != null) {
            compressionExecutor.shutdown();
        }
    }

    @Override
    public void reindexLatestEvents(final EventIndex eventIndex) {
        final List<WriteAheadStorePartition> partitions = getPartitions();
        final int numPartitions = partitions.size();

        final List<Future<?>> futures = new ArrayList<>(numPartitions);
        final ExecutorService executor = Executors.newFixedThreadPool(numPartitions);

        for (final WriteAheadStorePartition partition : partitions) {
            futures.add(executor.submit(() -> partition.reindexLatestEvents(eventIndex)));
        }

        executor.shutdown();
        for (final Future<?> future : futures) {
            try {
                future.get();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Failed to re-index events because Thread was interrupted", e);
            } catch (ExecutionException e) {
                throw new RuntimeException("Failed to re-index events", e);
            }
        }
    }

    @Override
    protected List<WriteAheadStorePartition> getPartitions() {
        return partitions;
    }

    @Override
    public EventIterator getEventsByTimestamp(final long minTimestamp, final long maxTimestamp) throws IOException {
        final List<EventIterator> eventIterators = new ArrayList<>();

        for (final WriteAheadStorePartition partition : getPartitions()) {
            final EventIterator partitionEventIterator = partition.getEventsByTimestamp(minTimestamp, maxTimestamp);
            eventIterators.add(partitionEventIterator);
        }

        return new AggregateEventIterator(eventIterators);
    }
}
