/*
 * Apache 2.0 License
 *
 * Copyright (c) Sebastian Katzer 2017
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apache License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/Apache-2.0/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 */

// codebeat:disable[TOO_MANY_FUNCTIONS]

package de.appplant.cordova.plugin.notification;

import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O;
import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT;
import static de.appplant.cordova.plugin.notification.Notification.PREF_KEY_ID;
import static de.appplant.cordova.plugin.notification.Notification.Type.TRIGGERED;

import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.service.notification.StatusBarNotification;

import androidx.core.app.NotificationManagerCompat;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import de.appplant.cordova.plugin.badge.BadgeImpl;

/**
 * Central way to access all or single local notifications set by specific
 * state like triggered or scheduled. Offers shortcut ways to schedule,
 * cancel or clear local notifications.
 */
public final class Manager {

  // TODO: temporary
  static final String CHANNEL_ID = "default-channel-id";

  // TODO: temporary
  private static final CharSequence CHANNEL_NAME = "Default channel";

  // The application context
  private Context context;

  /**
   * Constructor
   *
   * @param context Application context
   */
  private Manager(Context context) {
    this.context = context;
    createDefaultChannel();
  }

  /**
   * Static method to retrieve class instance.
   *
   * @param context Application context
   */
  public static Manager getInstance(Context context) {
    return new Manager(context);
  }

  /**
   * Check if app has local notification permission.
   */
  public boolean hasPermission() {
    return getNotCompMgr().areNotificationsEnabled();
  }

  /**
   * Schedule local notification specified by request.
   *
   * @param request  Set of notification options.
   * @param receiver Receiver to handle the trigger event.
   */
  public Notification schedule(Request request, Class<?> receiver) {
    Options options = request.getOptions();
    Notification toast = new Notification(context, options);

    toast.schedule(request, receiver);

    return toast;
  }

  /**
   * TODO: temporary
   */
  @SuppressLint("WrongConstant")
  private void createDefaultChannel() {
    NotificationManager mgr = getNotMgr();

    if (SDK_INT < O)
      return;

    NotificationChannel channel = mgr.getNotificationChannel(CHANNEL_ID);

    if (channel != null)
      return;

    channel = new NotificationChannel(
        CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_DEFAULT);

    mgr.createNotificationChannel(channel);
  }

  /**
   * Update local notification specified by ID.
   *
   * @param id       The notification ID.
   * @param updates  JSON object with notification options.
   * @param receiver Receiver to handle the trigger event.
   */
  public Notification update(int id, JSONObject updates, Class<?> receiver) {
    Notification notification = get(id);

    if (notification == null)
      return null;

    notification.update(updates, receiver);

    return notification;
  }

  /**
   * Clear local notification specified by ID.
   *
   * @param id The notification ID.
   */
  public Notification clear(int id) {
    Notification toast = get(id);

    if (toast != null) {
      toast.clear();
    }

    return toast;
  }

  /**
   * Clear all local notifications.
   */
  public void clearAll() {
    List<Notification> toasts = getByType(TRIGGERED);

    for (Notification toast : toasts) {
      toast.clear();
    }

    getNotCompMgr().cancelAll();
    setBadge(0);
  }

  /**
   * Clear local notification specified by ID.
   *
   * @param id The notification ID
   */
  public Notification cancel(int id) {
    Notification toast = get(id);

    if (toast != null) {
      toast.cancel();
    }

    return toast;
  }

  /**
   * Cancel all local notifications.
   */
  public void cancelAll() {
    List<Notification> notifications = getAll();

    for (Notification notification : notifications) {
      notification.cancel();
    }

    getNotCompMgr().cancelAll();
    setBadge(0);
  }

  /**
   * All local notifications IDs.
   */
  public List<Integer> getIds() {
    Set<String> keys = getPrefs().getAll().keySet();
    List<Integer> ids = new ArrayList<Integer>();

    for (String key : keys) {
      try {
        ids.add(Integer.parseInt(key));
      } catch (NumberFormatException e) {
        e.printStackTrace();
      }
    }

    return ids;
  }

  /**
   * All local notification IDs for given type.
   *
   * @param type The notification life cycle type
   */
  public List<Integer> getIdsByType(Notification.Type type) {

    if (type == Notification.Type.ALL)
      return getIds();

    StatusBarNotification[] activeToasts = getActiveNotifications();
    List<Integer> activeIds = new ArrayList<Integer>();

    for (StatusBarNotification toast : activeToasts) {
      activeIds.add(toast.getId());
    }

    if (type == TRIGGERED)
      return activeIds;

    List<Integer> ids = getIds();
    ids.removeAll(activeIds);

    return ids;
  }

  /**
   * List of local notifications with matching ID.
   *
   * @param ids Set of notification IDs.
   */
  private List<Notification> getByIds(List<Integer> ids) {
    List<Notification> toasts = new ArrayList<Notification>();

    for (int id : ids) {
      Notification toast = get(id);

      if (toast != null) {
        toasts.add(toast);
      }
    }

    return toasts;
  }

  /**
   * List of all local notification.
   */
  public List<Notification> getAll() {
    return getByIds(getIds());
  }

  /**
   * List of local notifications from given type.
   *
   * @param type The notification life cycle type
   */
  private List<Notification> getByType(Notification.Type type) {

    if (type == Notification.Type.ALL)
      return getAll();

    List<Integer> ids = getIdsByType(type);

    return getByIds(ids);
  }

  /**
   * List of properties from all local notifications.
   */
  public List<JSONObject> getOptions() {
    return getOptionsById(getIds());
  }

  /**
   * List of properties from local notifications with matching ID.
   *
   * @param ids Set of notification IDs
   */
  public List<JSONObject> getOptionsById(List<Integer> ids) {
    List<JSONObject> toasts = new ArrayList<JSONObject>();

    for (int id : ids) {
      Options options = getOptions(id);

      if (options != null) {
        toasts.add(options.getDict());
      }
    }

    return toasts;
  }

  /**
   * List of properties from all local notifications from given type.
   *
   * @param type The notification life cycle type
   */
  public List<JSONObject> getOptionsByType(Notification.Type type) {
    ArrayList<JSONObject> options = new ArrayList<JSONObject>();
    List<Notification> notifications = getByType(type);

    for (Notification notification : notifications) {
      options.add(notification.getOptions().getDict());
    }

    return options;
  }

  /**
   * Get local notification options.
   *
   * @param id Notification ID.
   * @return null if could not found.
   */
  public Options getOptions(int id) {
    SharedPreferences prefs = getPrefs();
    String toastId = Integer.toString(id);

    if (!prefs.contains(toastId))
      return null;

    try {
      String json = prefs.getString(toastId, null);
      JSONObject dict = new JSONObject(json);

      return new Options(context, dict);
    } catch (JSONException e) {
      e.printStackTrace();
      return null;
    }
  }

  /**
   * Get existent local notification.
   *
   * @param id Notification ID.
   * @return null if could not found.
   */
  public Notification get(int id) {
    Options options = getOptions(id);

    if (options == null)
      return null;

    return new Notification(context, options);
  }

  /**
   * Set the badge number of the app icon.
   *
   * @param badge The badge number.
   */
  public void setBadge(int badge) {
    if (badge == 0) {
      new BadgeImpl(context).clearBadge();
    } else {
      new BadgeImpl(context).setBadge(badge);
    }
  }

  /**
   * Get all active status bar notifications.
   */
  StatusBarNotification[] getActiveNotifications() {
    if (SDK_INT >= M) {
      return getNotMgr().getActiveNotifications();
    } else {
      return new StatusBarNotification[0];
    }
  }

  /**
   * Shared private preferences for the application.
   */
  private SharedPreferences getPrefs() {
    return context.getSharedPreferences(PREF_KEY_ID, Context.MODE_PRIVATE);
  }

  /**
   * Notification manager for the application.
   */
  private NotificationManager getNotMgr() {
    return (NotificationManager) context.getSystemService(
        Context.NOTIFICATION_SERVICE);
  }

  /**
   * Notification compat manager for the application.
   */
  private NotificationManagerCompat getNotCompMgr() {
    return NotificationManagerCompat.from(context);
  }

}

// codebeat:enable[TOO_MANY_FUNCTIONS]
