/*
 * Copyright 2018 Google LLC
 *
 * Licensed 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
 *
 *     https://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 com.google.android.gnd.repository;

import android.util.Log;
import androidx.annotation.NonNull;
import com.google.android.gnd.model.Project;
import com.google.android.gnd.model.feature.Feature;
import com.google.android.gnd.persistence.local.LocalDataStore;
import com.google.android.gnd.persistence.local.LocalValueStore;
import com.google.android.gnd.persistence.remote.RemoteDataStore;
import com.google.android.gnd.system.AuthenticationManager.User;
import com.google.common.collect.ImmutableSet;
import io.reactivex.Flowable;
import io.reactivex.Single;
import io.reactivex.processors.BehaviorProcessor;
import io.reactivex.processors.FlowableProcessor;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java8.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * Coordinates persistence and retrieval of {@link Project} instances from remote, local, and in
 * memory data stores. For more details on this pattern and overall architecture, see
 * https://developer.android.com/jetpack/docs/guide.
 */
@Singleton
public class ProjectRepository {
  private static final String TAG = ProjectRepository.class.getSimpleName();
  private static final long LOAD_REMOTE_PROJECT_TIMEOUT_SECS = 5;
  private static final long LOAD_REMOTE_PROJECT_SUMMARIES_TIMEOUT_SECS = 10;

  private final InMemoryCache cache;
  private final LocalDataStore localDataStore;
  private final RemoteDataStore remoteDataStore;
  private final Flowable<Loadable<Project>> activeProjectStream;
  private final FlowableProcessor<Optional<String>> activateProjectRequests;
  private final LocalValueStore localValueStore;

  @Inject
  public ProjectRepository(
      LocalDataStore localDataStore,
      RemoteDataStore remoteDataStore,
      InMemoryCache cache,
      LocalValueStore localValueStore) {
    this.localDataStore = localDataStore;
    this.remoteDataStore = remoteDataStore;
    this.cache = cache;
    this.localValueStore = localValueStore;

    // BehaviorProcessor re-emits last requested project id to late subscribers.
    this.activateProjectRequests = BehaviorProcessor.create();

    // Load project when requested id changes, caching the last loaded project.
    this.activeProjectStream =
        activateProjectRequests
            .distinctUntilChanged()
            .doOnNext(id -> Log.v(TAG, "Requested project id changed: " + id))
            .switchMap(this::onActivateProjectRequest)
            .replay(1)
            .refCount();
  }

  private Flowable<Loadable<Project>> onActivateProjectRequest(Optional<String> projectId) {
    // Empty id indicates intent to deactivate the current project. Used on sign out.
    if (projectId.isEmpty()) {
      return Flowable.just(Loadable.notLoaded());
    }
    String id = projectId.get();

    return syncProjectWithRemote(id)
        .onErrorResumeNext(error -> getProjectFromLocal(id, error))
        .doOnSuccess(__ -> localValueStore.setLastActiveProjectId(id))
        .toFlowable()
        .compose(Loadable::loadingOnceAndWrap);
  }

  private Single<Project> syncProjectWithRemote(String id) {
    return remoteDataStore
        .loadProject(id)
        .timeout(LOAD_REMOTE_PROJECT_TIMEOUT_SECS, TimeUnit.SECONDS)
        .doOnSubscribe(__ -> Log.d(TAG, "Activating project " + id))
        .doOnSuccess(localDataStore::insertOrUpdateProject);
  }

  private Single<Project> getProjectFromLocal(String id, Throwable error) {
    return localDataStore
        .getProjectById(id)
        .toSingle()
        .doOnSubscribe(__ -> Log.d(TAG, "Failed to load project from remote", error));
  }

  @NonNull
  public Optional<String> getLastActiveProjectId() {
    return Optional.ofNullable(localValueStore.getLastActiveProjectId());
  }

  /**
   * /** Returns a stream that emits the latest project activation state, and continues to emits
   * changes to that state until all subscriptions are disposed.
   */
  public Flowable<Loadable<Project>> getActiveProjectOnceAndStream() {
    return activeProjectStream;
  }

  public void activateProject(String projectId) {
    Log.v(TAG, "activateProject() called with " + projectId);
    activateProjectRequests.onNext(Optional.of(projectId));
  }

  public Flowable<Loadable<List<Project>>> getProjectSummaries(User user) {
    return loadProjectSummariesFromRemote(user)
        .onErrorResumeNext(error -> loadProjectSummariesFromLocal(error))
        .toFlowable()
        .compose(Loadable::loadingOnceAndWrap);
  }

  private Single<List<Project>> loadProjectSummariesFromRemote(User user) {
    return remoteDataStore
        .loadProjectSummaries(user)
        .timeout(LOAD_REMOTE_PROJECT_SUMMARIES_TIMEOUT_SECS, TimeUnit.SECONDS)
        .doOnSubscribe(__ -> Log.d(TAG, "Loading project list from remote"));
  }

  private Single<List<Project>> loadProjectSummariesFromLocal(Throwable error) {
    return localDataStore
        .getProjects()
        .doOnSubscribe(__ -> Log.d(TAG, "Failed to load project list from remote", error));
  }

  // TODO: Only return feature fields needed to render features on map.
  // TODO(#127): Decouple from Project and accept id instead.
  public Flowable<ImmutableSet<Feature>> getFeaturesOnceAndStream(Project project) {
    return localDataStore.getFeaturesOnceAndStream(project);
  }

  /** Clears the currently active project from cache and from local localValueStore. */
  public void clearActiveProject() {
    cache.clear();
    localValueStore.clearLastActiveProjectId();
    activateProjectRequests.onNext(Optional.empty());
  }
}

@@ -3438,64 +3438,225 @@
    .
-onErrorResumeNext(error -%3E getProjectFromLocal(id, error
+doOnSubscribe(__ -%3E Log.d(TAG, %22Activating project %22 + id))%0A        .doOnError(err -%3E Log.d(TAG, %22Failed to load project from remote%22, err))%0A        .onErrorResumeNext(__ -%3E localDataStore.getProjectById(id).toSingle(
 ))%0A 
@@ -4001,362 +4001,51 @@
 OnSu
-bscribe(__ -%3E Log.d(TAG, %22Activating project %22 + id))%0A        .doOnSuccess(localDataStore::insertOrUpdateProject);%0A  %7D%0A%0A  private Single%3CProject%3E getProjectFromLocal(String id, Throwable error) %7B%0A    return localDataStore%0A        .getProjectById(id)%0A        .toSingle()%0A        .doOnSubscribe(__ -%3E Log.d(TAG, %22Failed to load project from remote%22, error)
+ccess(localDataStore::insertOrUpdateProject
 );%0A 
@@ -4787,70 +4787,222 @@
    .
-onErrorResumeNext(error -%3E
+doOnSubscribe(__ -%3E Log.d(TAG, %22Loading project list from remote%22))%0A        .doOnError(err -%3E Log.d(TAG, %22Failed to
  load
-P
+ p
 roject
-SummariesFromLocal(error
+ list from remote%22, err))%0A        .onErrorResumeNext(__ -%3E localDataStore.getProjects(
 ))%0A 
@@ -5296,312 +5296,8 @@
 NDS)
-%0A        .doOnSubscribe(__ -%3E Log.d(TAG, %22Loading project list from remote%22));%0A  %7D%0A%0A  private Single%3CList%3CProject%3E%3E loadProjectSummariesFromLocal(Throwable error) %7B%0A    return localDataStore%0A        .getProjects()%0A        .doOnSubscribe(__ -%3E Log.d(TAG, %22Failed to load project list from remote%22, error))
 ;%0A  

Make project activation error reporting clearer