package com.atlassian.bitbucket.internal.tag.idx;

import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.bitbucket.util.TimerUtils;
import com.atlassian.util.contentcache.BackgroundThreadStreamPumper;
import com.atlassian.util.contentcache.ContentCache;
import com.atlassian.util.contentcache.ContentProvider;
import com.atlassian.util.contentcache.FileContentCacheManager;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Date;
import java.util.Set;

/**
 * Provides access to a cache of tags.
 */
public class TagIndexer {
    private static final String CACHE_KEY = "tag-index";

    /**
     * Directory in `BITBUCKET_HOME/caches` that will be used to stored the index files.
     */
    private static final String CACHE_PATH = CACHE_KEY;

    /**
     * Name of the background thread that will pump the data from the content provider to populate the cache's entry.
     */
    private static final String CACHE_PUMP_THREAD_NAME = CACHE_KEY + "-io-pump";

    /**
     * Controls how much space needs to be available on disk (specifically under <Bitbucket home directory>/caches) for
     * caching to be enabled. This setting ensures that the tags-index cache does not fill up the disk.
     * Units: Bytes
     */
    private static final String PROP_MIN_SPACE_IN_BYTES = "plugin.tag.index.cache.min.free.space";

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

    private final ContentCache cache;
    private final File tagCacheDirectory;
    private final BackgroundThreadStreamPumper cachePump;
    private final RefService refService;

    private volatile boolean active = false;

    public TagIndexer(ApplicationPropertiesService propertiesService, RefService refService) {
        this.refService = refService;
        //This is different to the directory passed to FileContentCacheManager because this class takes care of
        //cleaning up its cache directory and so needs the actual cache directory where its file are kept.
        tagCacheDirectory = new File(propertiesService.getCacheDir(), CACHE_PATH);
        cachePump = new BackgroundThreadStreamPumper(CACHE_PUMP_THREAD_NAME);
        int cacheMinSpaceInKb = propertiesService.getPluginProperty(PROP_MIN_SPACE_IN_BYTES, 1024 * 1024);
        cache = new FileContentCacheManager.Builder(propertiesService.getCacheDir())
                .minFreeSpaceBytes(cacheMinSpaceInKb)
                .streamPumper(cachePump)
                .build()
                .getCache(CACHE_KEY);

    }

    public void clearAll() {
        log.debug("clearing all indexes");
        cache.clear();
    }

    public void destroy() {
        active = false;
        clearCacheDirectory();
        cachePump.shutdown();
    }

    public void index(Repository repository) {
        log.debug("{}: generating index", repository);
        clearIndex(repository);
        // Trigger the write of the index by reading it
        readIndex(repository, 0, Collections.emptySet());
    }

    public void init() {
        clearCacheDirectory();
        active = true;
    }

    public Iterable<Tag> readIndex(Repository repository, int max, Set<String> commitIds) {
        // if the indexer is not active, cancel the read to not force the creation of the index
        if (!active) {
            log.debug("{}: skip reading index because the indexer was deactivated", repository);
            return Collections.emptyList();
        }

        log.debug("{}: reading index", repository);
        TagIndexReader reader = new TagIndexReader(commitIds, max);
        try (Timer ignored = TimerUtils.start("Reading index for repository '" + repository.getId() + "'")) {
            cache.stream(getCacheKey(repository), reader, new ContentProvider() {
                @Override
                public Date getExpiry() {
                    return null; // Never expire
                }

                @Override
                public void apply(OutputStream out) {
                    try (Timer ignored = TimerUtils.start("Writing index for repository '"
                            + repository.getId() + "'")) {
                        RepositoryTagsRequest request = new RepositoryTagsRequest.Builder(repository)
                                .order(RefOrder.ALPHABETICAL)
                                .build();
                        refService.streamTags(request, new TagIndexWriter(out));
                    }
                }
            });
        } catch (IOException e) {
            throw new RuntimeException("IOException", e);
        } finally {
            reader.close();
        }

        return reader.getMatches();
    }

    private void clearCacheDirectory() {
        try {
            FileUtils.cleanDirectory(tagCacheDirectory);
        } catch (IOException e) {
            if (log.isDebugEnabled()) {
                log.warn("Could not clear cache directory " + tagCacheDirectory.getAbsolutePath(), e);
            } else {
                log.warn("Could not clear cache directory " + tagCacheDirectory.getAbsolutePath() + ": " +
                        e.getMessage());
            }
        }
    }

    private void clearIndex(Repository repository) {
        log.debug("{}: clearing index", repository);
        cache.remove(getCacheKey(repository));
    }

    private String getCacheKey(Repository repository) {
        return Integer.toString(repository.getId());
    }
}
