package com.pr0gramm.app.services;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.pr0gramm.app.feed.FeedItem;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Very simple service to check if an item was already visited or not.
 */
@Singleton
public class SeenService {
    private static final Logger logger = LoggerFactory.getLogger("SeenService");

    private final Object lock = new Object();
    private final SettableFuture<ByteBuffer> buffer = SettableFuture.create();

    @Inject
    public SeenService(Context context) {
        File file = new File(context.getCacheDir(), "seen-posts.bits");

        AsyncTask.execute(() -> {
            try {
                buffer.set(mapByteBuffer(file));
            } catch (IOException error) {
                logger.warn("Could not load the seen-Cache");
            }
        });
    }

    public boolean isSeen(FeedItem item) {
        return isSeen(item.id());
    }

    public boolean isSeen(long id) {
        if (!this.buffer.isDone())
            return false;

        int idx = (int) id / 8;

        ByteBuffer buffer = Futures.getUnchecked(this.buffer);
        if (idx < 0 || idx >= buffer.limit()) {
            logger.warn("Id is too large");
            return false;
        }

        int mask = 1 << (7 - id % 8);
        return (buffer.get(idx) & mask) != 0;
    }

    public void markAsSeen(FeedItem item) {
        if (!this.buffer.isDone())
            return;

        int idx = (int) item.id() / 8;

        ByteBuffer buffer = Futures.getUnchecked(this.buffer);
        if (idx < 0 || idx >= buffer.limit()) {
            logger.warn("Id is too large");
            return;
        }

        // only one thread can write the buffer at a time.
        synchronized (lock) {
            byte value = buffer.get(idx);
            value |= 1 << (7 - item.id() % 8);
            buffer.put(idx, value);
        }
    }

    /**
     * Removes the "marked as seen" status from all items.
     */
    public void clear() {
        if (!this.buffer.isDone())
            return;

        ByteBuffer buffer = Futures.getUnchecked(this.buffer);

        synchronized (lock) {
            logger.info("Removing all the items");
            for (int idx = 0; idx < buffer.limit(); idx++) {
                buffer.put(idx, (byte) 0);
            }
        }
    }

    /**
     * Maps the cache into a byte buffer. The buffer is backed by the file, so
     * all changes to the buffer are written back to the file.
     *
     * @param file The file to map into memory
     */
    @SuppressLint("NewApi")
    private static ByteBuffer mapByteBuffer(File file) throws IOException {
        // space for up to two million posts
        final long size = 2_000_000 / 8;

        logger.info("Mapping cache: " + file);
        try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
            raf.setLength(size);
            return raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, size);
        }
    }
}

@@ -277,16 +277,101 @@
 edItem;%0A
+import com.pr0gramm.app.util.AndroidUtility;%0A%0Aimport org.apache.commons.io.FileUtils;
 %0Aimport 
@@ -1049,21 +1049,21 @@
 text.get
-Cache
+Files
 Dir(), %22
@@ -1116,16 +1116,183 @@
 () -%3E %7B%0A
+            // migrate old file if necessary%0A            File legacyFile = new File(context.getCacheDir(), %22seen-posts.bits%22);%0A            migrate(legacyFile, file);%0A%0A
         
@@ -1479,32 +1479,493 @@
      %7D);%0A    %7D%0A%0A
+    private void migrate(File legacyFile, File newFile) %7B%0A        if (legacyFile.exists() && !newFile.exists()) %7B%0A            logger.info(%22Moving seen-posts file from %7B%7D to %7B%7D%22, legacyFile, newFile);%0A%0A            try %7B%0A                FileUtils.moveFile(legacyFile, newFile);%0A            %7D catch (IOException error) %7B%0A                AndroidUtility.logToCrashlytics(new RuntimeException(%22Could not migrate 'seen' file.%22, error));%0A            %7D%0A        %7D%0A    %7D%0A%0A
     public boole
@@ -3788,11 +3788,12 @@
  to 
-two
+four
  mil
@@ -3833,9 +3833,9 @@
 e = 
-2
+4
 _000

Move 'seen-posts' file from cache to app storage.