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

import static com.google.common.base.Strings.isNullOrEmpty;
import static org.apache.jackrabbit.oak.commons.IOUtils.closeQuietly;
import static org.apache.jackrabbit.oak.osgi.OsgiUtil.lookupConfigurationThenFramework;
import static org.apache.jackrabbit.oak.segment.CachingSegmentReader.DEFAULT_STRING_CACHE_MB;
import static org.apache.jackrabbit.oak.segment.CachingSegmentReader.DEFAULT_TEMPLATE_CACHE_MB;
import static org.apache.jackrabbit.oak.segment.SegmentCache.DEFAULT_SEGMENT_CACHE_MB;
import static org.apache.jackrabbit.oak.segment.WriterCacheManager.DEFAULT_NODE_CACHE_SIZE_OSGi;
import static org.apache.jackrabbit.oak.segment.WriterCacheManager.DEFAULT_STRING_CACHE_SIZE_OSGi;
import static org.apache.jackrabbit.oak.segment.WriterCacheManager.DEFAULT_TEMPLATE_CACHE_SIZE_OSGi;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.DISABLE_ESTIMATION_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.FORCE_TIMEOUT_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.GC_PROGRESS_LOG_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.MEMORY_THRESHOLD_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.PAUSE_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.RETAINED_GENERATIONS_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.RETRY_COUNT_DEFAULT;
import static org.apache.jackrabbit.oak.segment.compaction.SegmentGCOptions.SIZE_DELTA_ESTIMATION_DEFAULT;
import static org.apache.jackrabbit.oak.segment.file.FileStoreBuilder.DEFAULT_MAX_FILE_SIZE;
import static org.apache.jackrabbit.oak.spi.blob.osgi.SplitBlobStoreService.ONLY_STANDALONE_TARGET;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import com.google.common.io.Closer;
import org.apache.jackrabbit.api.stats.RepositoryStatistics;
import org.apache.jackrabbit.api.stats.TimeSeries;
import org.apache.jackrabbit.oak.osgi.OsgiUtil;
import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
import org.apache.jackrabbit.oak.segment.spi.persistence.SegmentNodeStorePersistence;
import org.apache.jackrabbit.oak.spi.blob.BlobStore;
import org.apache.jackrabbit.oak.spi.state.NodeStoreProvider;
import org.apache.jackrabbit.oak.spi.whiteboard.Registration;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.apache.jackrabbit.oak.stats.CounterStats;
import org.apache.jackrabbit.oak.stats.HistogramStats;
import org.apache.jackrabbit.oak.stats.MeterStats;
import org.apache.jackrabbit.oak.stats.StatisticsProvider;
import org.apache.jackrabbit.oak.stats.StatsOptions;
import org.apache.jackrabbit.oak.stats.TimerStats;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A factory allowing creation of secondary segment node stores.
 * <p>
 * The different secondaries are distinguished by their role attribute.
 */
@Component(configurationPolicy = ConfigurationPolicy.REQUIRE)
@Designate(factory = true, ocd = SegmentNodeStoreFactory.Configuration.class)
public class SegmentNodeStoreFactory {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private static final long DEFAULT_BLOB_SNAPSHOT_INTERVAL = 12 * 60 * 60;

    private static final long DEFAULT_BLOB_GC_MAX_AGE = 24 * 60 * 60;

    @ObjectClassDefinition(
        name = "Apache Jackrabbit Oak Segment-Tar NodeStore Factory",
        description = "Factory allowing configuration of adjacent instances of " +
            "NodeStore implementation based on Segment model besides a default SegmentNodeStore in same setup."
    )
    @interface Configuration {

        @AttributeDefinition(
            name = "Repository Home Directory",
            description = "Path on the file system where repository data will be stored. "
                + "Defaults to the value of the framework property 'repository.home' or to 'repository' "
                + "if that is neither specified."
        )
        String repository_home() default "";

        @AttributeDefinition(
            name = "Mode",
            description = "TarMK mode (64 for memory mapped file access, 32 for normal file access). " +
                "Default value is taken from the 'sun.arch.data.model' system property."
        )
        String tarmk_mode() default "";

        @AttributeDefinition(
            name = "Maximum tar file size (MB)",
            description = "The maximum size of the tar files in megabytes. " +
                "Default value is '" + DEFAULT_MAX_FILE_SIZE + "'."
        )
        int tarmk_size() default DEFAULT_MAX_FILE_SIZE;

        @AttributeDefinition(
            name = "Segment cache size (MB)",
            description = "Cache size for storing most recently used segments in megabytes. " +
                "Default value is '" + DEFAULT_SEGMENT_CACHE_MB + "'."
        )
        int segmentCache_size() default DEFAULT_SEGMENT_CACHE_MB;

        @AttributeDefinition(
            name = "String cache size (MB)",
            description = "Cache size for storing most recently used strings in megabytes. " +
                "Default value is '" + DEFAULT_STRING_CACHE_MB + "'."
        )
        int stringCache_size() default DEFAULT_STRING_CACHE_MB;

        @AttributeDefinition(
            name = "Template cache size (MB)",
            description = "Cache size for storing most recently used templates in megabytes. " +
                "Default value is '" + DEFAULT_TEMPLATE_CACHE_MB + "'."
        )
        int templateCache_size() default DEFAULT_TEMPLATE_CACHE_MB;

        @AttributeDefinition(
            name = "String deduplication cache size (#items)",
            description = "Maximum number of strings to keep in the deduplication cache. " +
                "Default value is '" + DEFAULT_STRING_CACHE_SIZE_OSGi + "'."
        )
        int stringDeduplicationCache_size() default DEFAULT_STRING_CACHE_SIZE_OSGi;

        @AttributeDefinition(
            name = "Template deduplication cache size (#items)",
            description = "Maximum number of templates to keep in the deduplication cache. " +
                "Default value is '" + DEFAULT_TEMPLATE_CACHE_SIZE_OSGi + "'."
        )
        int templateDeduplicationCache_size() default DEFAULT_TEMPLATE_CACHE_SIZE_OSGi;

        @AttributeDefinition(
            name = "Node deduplication cache size (#items)",
            description = "Maximum number of node to keep in the deduplication cache. If the supplied " +
                "value is not a power of 2, it will be rounded up to the next power of 2. " +
                "Default value is '" + DEFAULT_NODE_CACHE_SIZE_OSGi + "'."
        )
        int nodeDeduplicationCache_size() default DEFAULT_NODE_CACHE_SIZE_OSGi;

        @AttributeDefinition(
            name = "Pause compaction",
            description = "When set to true the compaction phase is skipped during garbage collection. " +
                "Default value is '" + PAUSE_DEFAULT + "'."
        )
        boolean pauseCompaction() default PAUSE_DEFAULT;

        @AttributeDefinition(
            name = "Compaction retries",
            description = "Number of tries to compact concurrent commits on top of already " +
                "compacted commits. " +
                "Default value is '" + RETRY_COUNT_DEFAULT + "'."
        )
        int compaction_retryCount() default RETRY_COUNT_DEFAULT;

        @AttributeDefinition(
            name = "Force compaction timeout",
            description = "Number of seconds to attempt to force compact concurrent commits on top " +
                "of already compacted commits after the maximum number of retries has been " +
                "reached. Forced compaction tries to acquire an exclusive write lock on the " +
                "node store, blocking concurrent write access as long as the lock is held. " +
                "Default value is '" + FORCE_TIMEOUT_DEFAULT + "'."
        )
        int compaction_force_timeout() default FORCE_TIMEOUT_DEFAULT;

        @AttributeDefinition(
            name = "Garbage collection repository size threshold",
            description = "Garbage collection will be skipped unless the repository grew at least by " +
                "the number of bytes specified. " +
                "Default value is '" + SIZE_DELTA_ESTIMATION_DEFAULT + "'."
        )
        long compaction_sizeDeltaEstimation() default SIZE_DELTA_ESTIMATION_DEFAULT;

        @AttributeDefinition(
            name = "Disable estimation phase",
            description = "Disables the estimation phase allowing garbage collection to run unconditionally. " +
                "Default value is '" + DISABLE_ESTIMATION_DEFAULT + "'."
        )
        boolean compaction_disableEstimation() default DISABLE_ESTIMATION_DEFAULT;

        @AttributeDefinition(
            name = "Compaction retained generations",
            description = "Number of segment generations to retain during garbage collection. " +
                "The number of generations defaults to " + RETAINED_GENERATIONS_DEFAULT + " and " +
                "can't be changed. This configuration option is considered deprecated " +
                "and will be removed in the future."
        )
        int compaction_retainedGenerations() default RETAINED_GENERATIONS_DEFAULT;

        @AttributeDefinition(
            name = "Compaction memory threshold",
            description = "Threshold of available heap memory in percent of total heap memory below " +
                "which the compaction phase is canceled. 0 disables heap memory monitoring. " +
                "Default value is '" + MEMORY_THRESHOLD_DEFAULT + "'."
        )
        int compaction_memoryThreshold() default MEMORY_THRESHOLD_DEFAULT;

        @AttributeDefinition(
            name = "Compaction progress log",
            description = "The number of nodes compacted after which a status message is logged. " +
                "-1 disables progress logging. " +
                "Default value is '" + GC_PROGRESS_LOG_DEFAULT + "'."
        )
        long compaction_progressLog() default GC_PROGRESS_LOG_DEFAULT;

        @AttributeDefinition(
            name = "Standby mode",
            description = "Flag indicating this component will not register as a NodeStore but as a " +
                "NodeStoreProvider instead. " +
                "Default value is 'false'."
        )
        boolean standby() default false;

        @AttributeDefinition(
            name = "Custom blob store",
            description = "Boolean value indicating that a custom BlobStore is used for storing " +
                "large binary values."
        )
        boolean customBlobStore() default false;

        @AttributeDefinition(
            name = "Custom segment store",
            description = "Boolean value indicating that a custom (non-tar) segment store is used"
        )
        boolean customSegmentStore() default false;

        @AttributeDefinition(
                name = "Split persistence",
                description = "Boolean value indicating that the writes should be done locally when using the custom segment store"
        )
        boolean splitPersistence() default false;

        @AttributeDefinition(
            name = "Backup directory",
            description = "Directory (relative to current working directory) for storing repository backups. " +
                "Defaults to 'repository.home/segmentstore-backup'."
        )
        String repository_backup_dir() default "";

        @AttributeDefinition(
            name = "Blob gc max age (in secs)",
            description = "The blob garbage collection logic will only consider those blobs which " +
                "are not accessed recently (currentTime - lastModifiedTime > blobGcMaxAgeInSecs). " +
                "For example with the default setting only those blobs which have been created " +
                "at least 24 hours ago will be considered for garbage collection. " +
                "Default value is '" + DEFAULT_BLOB_GC_MAX_AGE + "'."
        )
        long blobGcMaxAgeInSecs() default DEFAULT_BLOB_GC_MAX_AGE;

        @AttributeDefinition(
            name = "Blob tracking snapshot interval",
            description = "Interval in seconds in which snapshots of locally tracked blob ids are " +
                "taken and synchronized with the blob store. This should be configured to be " +
                "less than the frequency of blob garbage collection so that deletions during blob " +
                "garbage collection can be accounted for in the next garbage collection execution. " +
                "Default value is '" + DEFAULT_BLOB_SNAPSHOT_INTERVAL + "'."
        )
        long blobTrackSnapshotIntervalInSecs() default DEFAULT_BLOB_SNAPSHOT_INTERVAL;

        @AttributeDefinition(
            name = "Role",
            description = "As multiple SegmentNodeStores can be configured, this parameter defines the role " +
                "of 'this' SegmentNodeStore."
        )
        String role() default "";

        @AttributeDefinition(
            name = "Register JCR descriptors as OSGi services",
            description = "Should only be done for one factory instance"
        )
        boolean registerDescriptors() default false;

        @AttributeDefinition(
                name = "Dispatch changes",
                description = "Enable support for observers on this factory instance"
        )
        boolean dispatchChanges() default false;
    }

    @Reference(
        cardinality = ReferenceCardinality.OPTIONAL,
        policy = ReferencePolicy.STATIC,
        policyOption = ReferencePolicyOption.GREEDY,
        target = ONLY_STANDALONE_TARGET
    )
    private volatile BlobStore blobStore;

    @Reference(
        cardinality = ReferenceCardinality.OPTIONAL,
        policy = ReferencePolicy.STATIC,
        policyOption = ReferencePolicyOption.GREEDY
    )
    private volatile SegmentNodeStorePersistence segmentStore;

    @Reference
    private StatisticsProvider statisticsProvider = StatisticsProvider.NOOP;

    private final Closer registrations = Closer.create();

    @Activate
    public void activate(ComponentContext context, Configuration configuration) throws IOException {
        log.info("activate: SegmentNodeStore '" + configuration.role() + "' starting.");

        if (configuration.role().isEmpty()) {
            return;
        }

        OsgiWhiteboard whiteboard = new OsgiWhiteboard(context.getBundleContext());
        SegmentNodeStore store = registerSegmentStore(
            context,
            configuration,
            blobStore,
            segmentStore,
            getRoleStatisticsProvider(statisticsProvider, configuration.role()),
            registrations,
            whiteboard,
            configuration.role(),
            log
        );

        if (store == null) {
            return;
        }

        Map<String, Object> props = new HashMap<String, Object>();
        props.put(NodeStoreProvider.ROLE, configuration.role());
        registrations.register(asCloseable(whiteboard.register(NodeStoreProvider.class, () -> store, props)));
        log.info("Registered NodeStoreProvider backed by SegmentNodeStore of type '{}'", configuration.role());
    }

    @Deactivate
    public void deactivate() {
        closeQuietly(registrations);
    }

    private static Closeable asCloseable(final Registration r) {
        return new Closeable() {

            @Override
            public void close() {
                r.unregister();
            }

        };
    }

    static String property(String name, ComponentContext context) {
        return lookupConfigurationThenFramework(context, name);
    }

    private static SegmentNodeStore registerSegmentStore(
        ComponentContext context,
        Configuration configuration,
        BlobStore blobStore,
        SegmentNodeStorePersistence segmentStore,
        StatisticsProvider statisticsProvider,
        Closer closer,
        Whiteboard whiteboard,
        String role,
        Logger logger
    ) throws IOException {
        return SegmentNodeStoreRegistrar.registerSegmentNodeStore(new SegmentNodeStoreRegistrar.Configuration() {

            String appendRole(String name) {
                return name + "-" + role;
            }

            int roundToNextPowerOfTwo(int size) {
                return 1 << (32 - Integer.numberOfLeadingZeros(Math.max(0, size - 1)));
            }

            String getMode() {
                String mode = configuration.tarmk_mode();
                if (isNullOrEmpty(mode)) {
                    return System.getProperty("tarmk.mode", System.getProperty("sun.arch.data.model", "32"));
                }
                return mode;
            }

            int getCacheSize(String name, int otherwise) {
                Integer size = Integer.getInteger(name);
                if (size != null) {
                    return size;
                }
                return otherwise;
            }

            @Override
            public boolean isPrimarySegmentStore() {
                return false;
            }

            @Override
            public boolean isSecondarySegmentStore() {
                return "secondary".equals(role);
            }

            @Override
            public boolean isStandbyInstance() {
                return configuration.standby();
            }

            @Override
            public String getRole() {
                return role;
            }

            @Override
            public int getRetainedGenerations() {
                return configuration.compaction_retainedGenerations();
            }

            @Override
            public int getDefaultRetainedGenerations() {
                return RETAINED_GENERATIONS_DEFAULT;
            }

            @Override
            public boolean getPauseCompaction() {
                return configuration.pauseCompaction();
            }

            @Override
            public int getRetryCount() {
                return configuration.compaction_retryCount();
            }

            @Override
            public int getForceCompactionTimeout() {
                return configuration.compaction_force_timeout();
            }

            @Override
            public long getSizeDeltaEstimation() {
                return configuration.compaction_sizeDeltaEstimation();
            }

            @Override
            public int getMemoryThreshold() {
                return configuration.compaction_memoryThreshold();
            }

            @Override
            public boolean getDisableEstimation() {
                return configuration.compaction_disableEstimation();
            }

            @Override
            public long getGCProcessLog() {
                return configuration.compaction_progressLog();
            }

            @Override
            public File getSegmentDirectory() {
                return new File(getRepositoryHome(), appendRole("segmentstore"));
            }

            @Override
            public File getSplitPersistenceDirectory() {
                return new File(getRepositoryHome(), appendRole("segmentstore-split"));
            }

            @Override
            public int getSegmentCacheSize() {
                return getCacheSize("segmentCache.size", configuration.segmentCache_size());
            }

            @Override
            public int getStringCacheSize() {
                return getCacheSize("stringCache.size", configuration.stringCache_size());
            }

            @Override
            public int getTemplateCacheSize() {
                return getCacheSize("templateCache.size", configuration.templateCache_size());
            }

            @Override
            public int getStringDeduplicationCacheSize() {
                return getCacheSize("stringDeduplicationCache.size", configuration.stringDeduplicationCache_size());
            }

            @Override
            public int getTemplateDeduplicationCacheSize() {
                return getCacheSize("templateDeduplicationCache.size", configuration.templateDeduplicationCache_size());
            }

            @Override
            public int getNodeDeduplicationCacheSize() {
                return roundToNextPowerOfTwo(getCacheSize("nodeDeduplicationCache.size", configuration.nodeDeduplicationCache_size()));
            }

            @Override
            public int getMaxFileSize() {
                return configuration.tarmk_size();
            }

            @Override
            public boolean getMemoryMapping() {
                return getMode().equals("64");
            }

            @Override
            public boolean hasCustomBlobStore() {
                return configuration.customBlobStore();
            }

            @Override
            public boolean hasCustomSegmentStore() {
                return configuration.customSegmentStore();
            }

            @Override
            public boolean hasSplitPersistence() {
                return configuration.splitPersistence();
            }

            @Override
            public boolean registerDescriptors() {
                return configuration.registerDescriptors();
            }

            @Override
            public boolean dispatchChanges() {
                return configuration.dispatchChanges();
            }

            @Override
            public String getRepositoryHome() {
                String repositoryHome = OsgiUtil.lookupConfigurationThenFramework(context, "repository.home");
                if (isNullOrEmpty(repositoryHome)) {
                    return "repository";
                }
                return repositoryHome;
            }

            @Override
            public long getBlobSnapshotInterval() {
                return configuration.blobTrackSnapshotIntervalInSecs();
            }

            @Override
            public long getBlobGcMaxAge() {
                return configuration.blobGcMaxAgeInSecs();
            }

            @Override
            public File getBackupDirectory() {
                String backupDirectory = configuration.repository_backup_dir();
                if (isNullOrEmpty(backupDirectory)) {
                    return new File(getRepositoryHome(), appendRole("segmentstore-backup"));
                }
                return new File(backupDirectory);
            }

            @Override
            public Whiteboard getWhiteboard() {
                return whiteboard;
            }

            @Override
            public Closer getCloser() {
                return closer;
            }

            @Override
            public Logger getLogger() {
                return logger;
            }

            @Override
            public StatisticsProvider getStatisticsProvider() {
                return statisticsProvider;
            }

            @Override
            public BlobStore getBlobStore() {
                return blobStore;
            }

            @Override
            public SegmentNodeStorePersistence getSegmentNodeStorePersistence() {
                return segmentStore;
            }

            @Override
            public BundleContext getBundleContext() {
                return context.getBundleContext();
            }

        });
    }

    private static StatisticsProvider getRoleStatisticsProvider(StatisticsProvider delegate, String role) {
        RepositoryStatistics repositoryStatistics = new RepositoryStatistics() {

            @Override
            public TimeSeries getTimeSeries(Type type) {
                return getTimeSeries(type.name(), type.isResetValueEachSecond());
            }

            @Override
            public TimeSeries getTimeSeries(String type, boolean resetValueEachSecond) {
                return delegate.getStats().getTimeSeries(addRoleToName(type, role), resetValueEachSecond);
            }
        };

        return new StatisticsProvider() {

            @Override
            public RepositoryStatistics getStats() {
                return repositoryStatistics;
            }

            @Override
            public MeterStats getMeter(String name, StatsOptions options) {
                return delegate.getMeter(addRoleToName(name, role), options);
            }

            @Override
            public CounterStats getCounterStats(String name, StatsOptions options) {
                return delegate.getCounterStats(addRoleToName(name, role), options);
            }

            @Override
            public TimerStats getTimer(String name, StatsOptions options) {
                return delegate.getTimer(addRoleToName(name, role), options);
            }

            @Override
            public HistogramStats getHistogram(String name, StatsOptions options) {
                return delegate.getHistogram(addRoleToName(name, role), options);
            }

        };
    }

    private static String addRoleToName(String name, String role) {
        return role + '.' + name;
    }

}
