package org.thoughtcrime.securesms.megaphone;

import android.content.Context;

import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;

import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MegaphoneDatabase;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

/**
 * Synchronization of data structures is done using a serial executor. Do not access or change
 * data structures or fields on anything except the executor.
 */
public class MegaphoneRepository {

  private final Context                     context;
  private final Executor                    executor;
  private final MegaphoneDatabase           database;
  private final Map<Event, MegaphoneRecord> databaseCache;

  private boolean enabled;

  public MegaphoneRepository(@NonNull Context context) {
    this.context       = context;
    this.executor      = SignalExecutors.SERIAL;
    this.database      = DatabaseFactory.getMegaphoneDatabase(context);
    this.databaseCache = new HashMap<>();

    executor.execute(this::init);
  }

  /**
   * Marks any megaphones a new user shouldn't see as "finished".
   */
  @AnyThread
  public void onFirstEverAppLaunch() {
    executor.execute(() -> {
      database.markFinished(Event.REACTIONS);
      database.markFinished(Event.MESSAGE_REQUESTS);
      resetDatabaseCache();
    });
  }

  @AnyThread
  public void onAppForegrounded() {
    executor.execute(() -> enabled = true);
  }

  @AnyThread
  public void getNextMegaphone(@NonNull Callback<Megaphone> callback) {
    executor.execute(() -> {
      if (enabled) {
        init();
        callback.onResult(Megaphones.getNextMegaphone(context, databaseCache));
      } else {
        callback.onResult(null);
      }
    });
  }

  @AnyThread
  public void markVisible(@NonNull Megaphones.Event event) {
    long time = System.currentTimeMillis();

    executor.execute(() -> {
      if (getRecord(event).getFirstVisible() == 0) {
        database.markFirstVisible(event, time);
        resetDatabaseCache();
      }
    });
  }

  @AnyThread
  public void markSeen(@NonNull Event event) {
    long lastSeen = System.currentTimeMillis();

    executor.execute(() -> {
      MegaphoneRecord record = getRecord(event);
      database.markSeen(event, record.getSeenCount() + 1, lastSeen);
      enabled = false;
      resetDatabaseCache();
    });
  }

  @AnyThread
  public void markFinished(@NonNull Event event) {
    executor.execute(() -> {
      database.markFinished(event);
      resetDatabaseCache();
    });
  }

  @WorkerThread
  private void init() {
    List<MegaphoneRecord> records = database.getAllAndDeleteMissing();
    Set<Event>            events  = Stream.of(records).map(MegaphoneRecord::getEvent).collect(Collectors.toSet());
    Set<Event>            missing = Stream.of(Megaphones.Event.values()).filterNot(events::contains).collect(Collectors.toSet());

    database.insert(missing);
    resetDatabaseCache();
  }

  @WorkerThread
  private @NonNull MegaphoneRecord getRecord(@NonNull Event event) {
    //noinspection ConstantConditions
    return databaseCache.get(event);
  }

  @WorkerThread
  private void resetDatabaseCache() {
    databaseCache.clear();
    databaseCache.putAll(Stream.of(database.getAllAndDeleteMissing()).collect(Collectors.toMap(MegaphoneRecord::getEvent, m -> m)));
  }

  public interface Callback<E> {
    void onResult(E result);
  }
}