/*
 * Copyright (C) 2020 The Dagger Authors.
 *
 * 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
 *
 * 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 dagger.hilt.android.internal.managers;

import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.activity.ComponentActivity;
import dagger.Binds;
import dagger.Module;
import dagger.hilt.EntryPoint;
import dagger.hilt.EntryPoints;
import dagger.hilt.InstallIn;
import dagger.hilt.android.ActivityRetainedLifecycle;
import dagger.hilt.android.components.ActivityRetainedComponent;
import dagger.hilt.android.components.ApplicationComponent;
import dagger.hilt.android.internal.ThreadUtil;
import dagger.hilt.android.internal.builders.ActivityRetainedComponentBuilder;
import dagger.hilt.android.scopes.ActivityRetainedScoped;
import dagger.hilt.internal.GeneratedComponentManager;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;

/** A manager for the creation of components that survives activity configuration changes. */
final class ActivityRetainedComponentManager
    implements GeneratedComponentManager<ActivityRetainedComponent> {

  /** Entry point for {@link ActivityRetainedComponentBuilder}. */
  @EntryPoint
  @InstallIn(ApplicationComponent.class)
  public interface ActivityRetainedComponentBuilderEntryPoint {
    ActivityRetainedComponentBuilder retainedComponentBuilder();
  }

  /** Entry point for {@link Lifecycle}. */
  @EntryPoint
  @InstallIn(ActivityRetainedComponent.class)
  public interface ActivityRetainedLifecycleEntryPoint {
    ActivityRetainedLifecycle getActivityRetainedLifecycle();
  }

  static final class ActivityRetainedComponentViewModel extends ViewModel {
    private final ActivityRetainedComponent component;

    ActivityRetainedComponentViewModel(ActivityRetainedComponent component) {
      this.component = component;
    }

    ActivityRetainedComponent getComponent() {
      return component;
    }

    @Override
    protected void onCleared() {
      super.onCleared();
      ActivityRetainedLifecycle lifecycle =
          EntryPoints.get(component, ActivityRetainedLifecycleEntryPoint.class)
              .getActivityRetainedLifecycle();
      ((ActivityRetainedComponentManager.Lifecycle) lifecycle).dispatchOnCleared();
    }
  }

  private final ViewModelProvider viewModelProvider;

  @Nullable private volatile ActivityRetainedComponent component;
  private final Object componentLock = new Object();

  ActivityRetainedComponentManager(ComponentActivity activity) {
    this.viewModelProvider =
        new ViewModelProvider(
            activity,
            new ViewModelProvider.Factory() {
              @NonNull
              @Override
              @SuppressWarnings("unchecked")
              public <T extends ViewModel> T create(@NonNull Class<T> aClass) {
                ActivityRetainedComponent component =
                    EntryPoints.get(
                            activity.getApplication(),
                            ActivityRetainedComponentBuilderEntryPoint.class)
                        .retainedComponentBuilder()
                        .build();
                return (T) new ActivityRetainedComponentViewModel(component);
              }
            });
  }

  @Override
  public ActivityRetainedComponent generatedComponent() {
    if (component == null) {
      synchronized (componentLock) {
        if (component == null) {
          component = createComponent();
        }
      }
    }
    return component;
  }

  private ActivityRetainedComponent createComponent() {
    return viewModelProvider.get(ActivityRetainedComponentViewModel.class).getComponent();
  }

  /** The default implementation of {@link ActivityRetainedLifecycle}. */
  @ActivityRetainedScoped
  static final class Lifecycle implements ActivityRetainedLifecycle {

    private final Set<OnClearedListener> listeners = new HashSet<>();

    @Inject
    Lifecycle() {}

    @Override
    public void addOnClearedListener(@NonNull OnClearedListener listener) {
      ThreadUtil.ensureMainThread();
      listeners.add(listener);
    }

    @Override
    public void removeOnClearedListener(@NonNull OnClearedListener listener) {
      ThreadUtil.ensureMainThread();
      listeners.remove(listener);
    }

    void dispatchOnCleared() {
      for (OnClearedListener listener : listeners) {
        listener.onCleared();
      }
    }
  }

  @Module
  @InstallIn(ActivityRetainedComponent.class)
  abstract static class LifecycleModule {
    @Binds
    abstract ActivityRetainedLifecycle bind(Lifecycle impl);
  }
}

@@ -4454,16 +4454,65 @@
 Set%3C%3E();
+%0A    private boolean onClearedDispatched = false;
 %0A%0A    @I
@@ -4656,32 +4656,68 @@
 reMainThread();%0A
+      throwIfOnClearedDispatched();%0A
       listeners.
@@ -4864,24 +4864,60 @@
 inThread();%0A
+      throwIfOnClearedDispatched();%0A
       listen
@@ -4972,24 +4972,95 @@
 Cleared() %7B%0A
+      ThreadUtil.ensureMainThread();%0A      onClearedDispatched = true;%0A
       for (O
@@ -5139,24 +5139,403 @@
      %7D%0A    %7D
+%0A%0A    private void throwIfOnClearedDispatched() %7B%0A      if (onClearedDispatched) %7B%0A        throw new IllegalStateException(%0A            %22There was a race between the call to add/remove an OnClearedListener and onCleared(). %22%0A                + %22This can happen when posting to the Main thread from a background thread, %22%0A                + %22which is not supported.%22);%0A      %7D%0A    %7D
 %0A  %7D%0A%0A  @Mod

Throw when a race is detected adding a OnClearedListener.