/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.geckoview;

import android.os.Parcel;
import android.os.ParcelFormatException;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.os.ParcelCompat;
import java.util.Arrays;
import java.util.Objects;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.util.ThreadUtils;

/**
 * This class represents a single <a
 * href="https://developer.mozilla.org/en-US/docs/Web/API/Notification">Web Notification</a>. These
 * can be received by connecting a {@link WebNotificationDelegate} to {@link GeckoRuntime} via
 * {@link GeckoRuntime#setWebNotificationDelegate(WebNotificationDelegate)}.
 */
public class WebNotification implements Parcelable {

  /**
   * Title is shown at the top of the notification window.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/title">Web
   *     Notification - title</a>
   */
  public final @Nullable String title;

  /** Tag is the ID of the notification, matching `nsIAlertNotification.name`. */
  public final @NonNull String tag;

  private final @Nullable String mCookie;

  /**
   * Text represents the body of the notification.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/body">Web
   *     Notification - text</a>
   */
  public final @Nullable String text;

  /**
   * ImageURL contains the URL of an icon to be displayed as part of the notification.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/icon">Web
   *     Notification - icon</a>
   */
  public final @Nullable String imageUrl;

  /**
   * TextDirection indicates the direction that the language of the text is displayed. Possible
   * values are: auto: adopts the browser's language setting behaviour (the default.) ltr: left to
   * right. rtl: right to left.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/dir">Web
   *     Notification - dir</a>
   */
  public final @Nullable String textDirection;

  /**
   * Lang indicates the notification's language, as specified using a DOMString representing a BCP
   * 47 language tag.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMString">DOM String</a>
   * @see <a href="http://www.rfc-editor.org/rfc/bcp/bcp47.txt">BCP 47</a>
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/lang">Web
   *     Notification - lang</a>
   */
  public final @Nullable String lang;

  /**
   * RequireInteraction indicates whether a notification should remain active until the user clicks
   * or dismisses it, rather than closing automatically.
   *
   * @see <a
   *     href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/requireInteraction">Web
   *     Notification - requireInteraction</a>
   */
  public final boolean requireInteraction;

  /**
   * This is the URL of the page or Service Worker that generated the notification. Null if this
   * notification was not generated by a Web Page (e.g. from an Extension).
   *
   * <p>TODO: make NonNull once we have Bug 1589693
   */
  public final @Nullable String source;

  /**
   * When set, indicates that no sounds or vibrations should be made.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/silent">Web
   *     Notification - silent</a>
   */
  public final boolean silent;

  /** indicates whether the notification came from private browsing mode or not. */
  public final boolean privateBrowsing;

  /**
   * A vibration pattern to run with the display of the notification. A vibration pattern can be an
   * array with as few as one member. The values are times in milliseconds where the even indices
   * (0, 2, 4, etc.) indicate how long to vibrate and the odd indices indicate how long to pause.
   * For example, [300, 100, 400] would vibrate 300ms, pause 100ms, then vibrate 400ms.
   *
   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Notification/vibrate">Web
   *     Notification - vibrate</a>
   */
  public final @NonNull int[] vibrate;

  /** Array of actions available for this notification. */
  public final @NonNull WebNotificationAction[] actions;

  /**
   * Similar to {@link #source} but includes the full origin information, corresponding to
   * `nsIPrincipal.origin`.
   */
  public final @NonNull String origin;

  @WrapForJNI
  /* package */ WebNotification(
      @Nullable final String title,
      @NonNull final String tag,
      @Nullable final String cookie,
      @Nullable final String text,
      @Nullable final String imageUrl,
      @Nullable final String textDirection,
      @Nullable final String lang,
      final boolean requireInteraction,
      @NonNull final String source,
      final boolean silent,
      final boolean privateBrowsing,
      @NonNull final int[] vibrate,
      @NonNull final Object[] actions,
      @NonNull final String origin) {
    this.tag = tag;
    this.mCookie = cookie;
    this.title = title;
    this.text = text;
    this.imageUrl = imageUrl;
    this.textDirection = textDirection;
    this.lang = lang;
    this.requireInteraction = requireInteraction;
    this.source = source.isEmpty() ? null : source;
    this.silent = silent;
    this.vibrate = vibrate;
    this.privateBrowsing = privateBrowsing;
    this.actions = Arrays.copyOf(actions, actions.length, WebNotificationAction[].class);
    this.origin = origin;
  }

  /**
   * This should be called when the app starts showing the notification. This is important, as it
   * tells the result of the notification request to Web Content.
   */
  @UiThread
  public void show() {
    ThreadUtils.assertOnUiThread();
    GeckoAppShell.onNotificationShow(tag, mCookie, origin);
  }

  /**
   * This should be called when the user taps or clicks a notification. Note that this does not
   * automatically dismiss the notification as far as Web Content is concerned. For that, see {@link
   * #dismiss()}.
   */
  @UiThread
  public void click() {
    ThreadUtils.assertOnUiThread();
    GeckoAppShell.onNotificationClick(tag, null, origin);
  }

  /**
   * This should be called when the user taps or clicks a notification action. Note that this does
   * not automatically dismiss the notification as far as Web Content is concerned. For that, see
   * {@link #dismiss()}.
   *
   * @param action The action name if an action button is clicked, otherwise null.
   */
  @UiThread
  public void click(final @NonNull String action) {
    ThreadUtils.assertOnUiThread();
    GeckoAppShell.onNotificationClick(tag, action, origin);
  }

  /**
   * This should be called when the app stops showing the notification. This is important, as there
   * may be a limit to the number of active notifications each site can display.
   */
  @UiThread
  public void dismiss() {
    ThreadUtils.assertOnUiThread();
    GeckoAppShell.onNotificationClose(tag, origin);
  }

  // Increment this value whenever anything changes in the parcelable representation.
  private static final int VERSION = 3;

  // To avoid TransactionTooLargeException, we only store small imageUrls
  private static final int IMAGE_URL_LENGTH_MAX = 150;

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(final Parcel dest, final int flags) {
    dest.writeInt(VERSION);
    dest.writeString(title);
    dest.writeString(tag);
    dest.writeString(mCookie);
    dest.writeString(text);
    if (imageUrl != null && imageUrl.length() < IMAGE_URL_LENGTH_MAX) {
      dest.writeString(imageUrl);
    } else {
      dest.writeString("");
    }
    dest.writeString(textDirection);
    dest.writeString(lang);
    dest.writeInt(requireInteraction ? 1 : 0);
    dest.writeString(source);
    dest.writeInt(silent ? 1 : 0);
    dest.writeInt(privateBrowsing ? 1 : 0);
    dest.writeIntArray(vibrate);
    dest.writeParcelableArray(actions, 0);
    dest.writeString(origin);
  }

  private WebNotification(final Parcel in) {
    final int version = in.readInt();
    if (version < 1 || version > 3) {
      throw new ParcelFormatException("Mismatched version: " + version + " expected: " + VERSION);
    }
    title = in.readString();
    tag = Objects.requireNonNull(in.readString());
    mCookie = in.readString();
    text = in.readString();
    imageUrl = in.readString();
    textDirection = in.readString();
    lang = in.readString();
    requireInteraction = in.readInt() == 1;
    source = in.readString();
    silent = in.readInt() == 1;
    privateBrowsing = in.readInt() == 1;
    vibrate = Objects.requireNonNull(in.createIntArray());

    // TODO: This exists for existing notifications opened from older releases before app updates.
    // We should be able to remove version 1 support after a few release cycles.
    // See bug 1970892.
    if (version == 1) {
      actions = new WebNotificationAction[0];
      origin = "";
      return;
    }

    final Parcelable[] actionParcels =
        Objects.requireNonNull(
            ParcelCompat.readParcelableArrayTyped(
                in, WebNotificationAction.class.getClassLoader(), WebNotificationAction.class));
    actions = Arrays.copyOf(actionParcels, actionParcels.length, WebNotificationAction[].class);

    if (version == 2) {
      origin = "";
      return;
    }

    origin = Objects.requireNonNull(in.readString());
  }

  /** Parcelable creator for WebNotification instances. */
  public static final Creator<WebNotification> CREATOR =
      new Creator<>() {
        @Override
        public WebNotification createFromParcel(final Parcel in) {
          return new WebNotification(in);
        }

        @Override
        public WebNotification[] newArray(final int size) {
          return new WebNotification[size];
        }
      };
}
