/*
 * Copyright 2018-present MongoDB, Inc.
 *
 * 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 com.mongodb.stitch.android.core.auth.internal;

import android.os.Build;
import com.google.android.gms.tasks.Task;
import com.mongodb.stitch.android.core.Stitch;
import com.mongodb.stitch.android.core.auth.StitchAuth;
import com.mongodb.stitch.android.core.auth.StitchAuthListener;
import com.mongodb.stitch.android.core.auth.StitchUser;
import com.mongodb.stitch.android.core.auth.providers.internal.AuthProviderClientFactory;
import com.mongodb.stitch.android.core.auth.providers.internal.NamedAuthProviderClientFactory;
import com.mongodb.stitch.android.core.internal.common.TaskDispatcher;
import com.mongodb.stitch.core.StitchAppClientInfo;
import com.mongodb.stitch.core.auth.StitchCredential;
import com.mongodb.stitch.core.auth.internal.CoreStitchAuth;
import com.mongodb.stitch.core.auth.internal.CoreStitchUser;
import com.mongodb.stitch.core.auth.internal.DeviceFields;
import com.mongodb.stitch.core.auth.internal.StitchAuthRoutes;
import com.mongodb.stitch.core.auth.internal.StitchUserFactory;
import com.mongodb.stitch.core.internal.common.Storage;
import com.mongodb.stitch.core.internal.net.StitchRequestClient;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.annotation.Nullable;
import org.bson.Document;

/**
 * The Android specific authentication component for clients that acts as a {@link StitchAuth} and
 * an {@link com.mongodb.stitch.core.auth.internal.StitchAuthRequestClient}.
 */
public final class StitchAuthImpl extends CoreStitchAuth<StitchUser> implements StitchAuth {
  private final TaskDispatcher dispatcher;
  private final StitchAppClientInfo appInfo;
  /**
   * A set of auth listeners that should be dispatched to asynchronously
   */
  private final ConcurrentMap<StitchAuthListener, Boolean> listeners =
      new ConcurrentHashMap<>();
  /**
   * A set of auth listeners that should be dispatched to synchronously.
   * For internal use.
   */
  private final ConcurrentMap<StitchAuthListener, Boolean> synchronousListeners =
      new ConcurrentHashMap<>();

  /**
   * Constructs a {@link StitchAuthImpl}.
   *
   * @param requestClient the request client to use for any Stitch requests.
   * @param authRoutes auth specific routes.
   * @param storage where to store/retrieve authentication data.
   * @param dispatcher where to send asynchronous requests to.
   * @param appInfo information about the application.
   */
  public StitchAuthImpl(
      final StitchRequestClient requestClient,
      final StitchAuthRoutes authRoutes,
      final Storage storage,
      final TaskDispatcher dispatcher,
      final StitchAppClientInfo appInfo) {
    super(requestClient, authRoutes, storage, true);
    this.dispatcher = dispatcher;
    this.appInfo = appInfo;
  }

  protected StitchUserFactory<StitchUser> getUserFactory() {
    return new StitchUserFactoryImpl(this);
  }

  @Override
  public <ClientT> ClientT getProviderClient(
          final AuthProviderClientFactory<ClientT> factory) {
    return factory.getClient(this, getRequestClient(), getAuthRoutes(), dispatcher);
  }

  @Override
  public <T> T getProviderClient(
      final NamedAuthProviderClientFactory<T> factory, final String providerName) {
    return factory.getClient(providerName, getRequestClient(), getAuthRoutes(), dispatcher);
  }

  @Override
  public Task<StitchUser> loginWithCredential(final StitchCredential credential) {
    return dispatcher.dispatchTask(
        new Callable<StitchUser>() {
          @Override
          public StitchUser call() {
            return loginWithCredentialInternal(credential);
          }
        });
  }

  Task<StitchUser> linkWithCredential(
      final CoreStitchUser user, final StitchCredential credential) {
    return dispatcher.dispatchTask(
        new Callable<StitchUser>() {
          @Override
          public StitchUser call() {
            return linkUserWithCredentialInternal(user, credential);
          }
        });
  }

  @Override
  public Task<Void> logout() {
    return dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            logoutInternal();
            return null;
          }
        });
  }

  @Override
  public Task<Void> logoutUserWithId(final String userId) {
    return dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            logoutUserWithIdInternal(userId);
            return null;
          }
        });
  }

  @Override
  public Task<Void> removeUser() {
    return dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            removeUserInternal();
            return null;
          }
        });
  }

  @Override
  public Task<Void> removeUserWithId(final String userId) {
    return dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            removeUserWithIdInternal(userId);
            return null;
          }
        });
  }

  @Override
  protected Document getDeviceInfo() {
    final Document info = super.getDeviceInfo();
    if (appInfo.getLocalAppName() != null) {
      info.put(DeviceFields.APP_ID, appInfo.getLocalAppName());
    }
    if (appInfo.getLocalAppVersion() != null) {
      info.put(DeviceFields.APP_VERSION, appInfo.getLocalAppVersion());
    }
    info.put(DeviceFields.PLATFORM, "android");
    info.put(DeviceFields.PLATFORM_VERSION, Build.VERSION.RELEASE);

    final String packageVersion = Stitch.class.getPackage().getImplementationVersion();
    if (packageVersion != null && !packageVersion.isEmpty()) {
      info.put(DeviceFields.SDK_VERSION, packageVersion);
    }

    return info;
  }

  /**
   * Adds a listener for any important auth event.
   *
   * @see StitchAuthListener
   */
  public void addAuthListener(final StitchAuthListener listener) {
    listeners.put(listener, Boolean.TRUE);

    // Trigger the onUserLoggedIn event in case some event happens and
    // this caller would miss out on this event other wise.
    onAuthEvent(listener);
    dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            listener.onListenerRegistered(StitchAuthImpl.this);
            return null;
          }
        });
  }

  public void addSynchronousAuthListener(final StitchAuthListener listener) {
    synchronousListeners.put(listener, Boolean.TRUE);

    // Trigger the onUserLoggedIn event in case some event happens and
    // this caller would miss out on this event other wise.
    onAuthEvent(listener);
    listener.onListenerRegistered(this);
  }

  /**
   * Removes a listener.
   *
   * @see StitchAuthListener
   */
  public void removeAuthListener(final StitchAuthListener listener) {
    listeners.remove(listener);
  }

  private void onAuthEvent(final StitchAuthListener listener) {
    final StitchAuth auth = this;
    dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() {
            listener.onAuthEvent(auth);
            return null;
          }
        });
  }

  @Override
  protected void onAuthEvent() {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onAuthEvent(StitchAuthImpl.this);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onAuthEvent(this);
    }
  }

  @Override
  protected void onListenerInitialized() {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onListenerRegistered(StitchAuthImpl.this);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onListenerRegistered(this);
    }
  }

  @Override
  protected void onActiveUserChanged(@Nullable final StitchUser currentActiveUser,
                                     @Nullable final StitchUser previousActiveUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onActiveUserChanged(
                  StitchAuthImpl.this, currentActiveUser, previousActiveUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onActiveUserChanged(
          this, currentActiveUser, previousActiveUser);
    }
  }

  @Override
  protected void onUserAdded(final StitchUser createdUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onUserAdded(
                  StitchAuthImpl.this, createdUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onUserAdded(this, createdUser);
    }
  }

  @Override
  protected void onUserLoggedIn(final StitchUser loggedInUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onUserLoggedIn(
                  StitchAuthImpl.this, loggedInUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onUserLoggedIn(this, loggedInUser);
    }
  }

  @Override
  protected void onUserRemoved(final StitchUser removedUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onUserRemoved(StitchAuthImpl.this, removedUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onUserRemoved(this, removedUser);
    }
  }

  @Override
  protected void onUserLoggedOut(final StitchUser loggedOutUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onUserLoggedOut(StitchAuthImpl.this, loggedOutUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onUserLoggedOut(this, loggedOutUser);
    }
  }

  @Override
  protected void onUserLinked(final StitchUser linkedUser) {
    for (final StitchAuthListener listener : listeners.keySet()) {
      dispatcher.dispatchTask(
          new Callable<Void>() {
            @Override
            public Void call() {
              listener.onUserLinked(StitchAuthImpl.this, linkedUser);
              return null;
            }
          });
    }
    for (final StitchAuthListener listener : synchronousListeners.keySet()) {
      listener.onUserLinked(this, linkedUser);
    }
  }

  @Override
  public Task<Void> refreshCustomData() {
    return dispatcher.dispatchTask(
        new Callable<Void>() {
          @Override
          public Void call() throws Exception {
            refreshAccessToken();
            return null;
          }
        }
    );
  }
}