/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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.google.android.exoplayer2.util;

import android.os.Looper;
import android.os.Message;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import java.util.ArrayDeque;
import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.NonNull;

/**
 * A set of listeners.
 *
 * <p>Events are guaranteed to arrive in the order in which they happened even if a new event is
 * triggered recursively from another listener.
 *
 * <p>Events are also guaranteed to be only sent to the listeners registered at the time the event
 * was enqueued and haven't been removed since.
 *
 * @param <T> The listener type.
 */
public final class ListenerSet<T extends @NonNull Object> {

  /**
   * An event sent to a listener.
   *
   * @param <T> The listener type.
   */
  public interface Event<T> {

    /** Invokes the event notification on the given listener. */
    void invoke(T listener);
  }

  /**
   * An event sent to a listener when all other events sent during one {@link Looper} message queue
   * iteration were handled by the listener.
   *
   * @param <T> The listener type.
   */
  public interface IterationFinishedEvent<T> {

    /**
     * Invokes the iteration finished event.
     *
     * @param listener The listener to invoke the event on.
     * @param eventFlags The combined event {@link FlagSet flags} of all events sent in this
     *     iteration.
     */
    void invoke(T listener, FlagSet eventFlags);
  }

  private static final int MSG_ITERATION_FINISHED = 0;

  private final Clock clock;
  private final HandlerWrapper handler;
  private final IterationFinishedEvent<T> iterationFinishedEvent;
  private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
  private final ArrayDeque<Runnable> flushingEvents;
  private final ArrayDeque<Runnable> queuedEvents;

  private boolean released;

  /**
   * Creates a new listener set.
   *
   * @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used
   *     to call all other methods of this class.
   * @param clock A {@link Clock}.
   * @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
   *     during one {@link Looper} message queue iteration were handled by the listeners.
   */
  public ListenerSet(Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) {
    this(/* listeners= */ new CopyOnWriteArraySet<>(), looper, clock, iterationFinishedEvent);
  }

  private ListenerSet(
      CopyOnWriteArraySet<ListenerHolder<T>> listeners,
      Looper looper,
      Clock clock,
      IterationFinishedEvent<T> iterationFinishedEvent) {
    this.clock = clock;
    this.listeners = listeners;
    this.iterationFinishedEvent = iterationFinishedEvent;
    flushingEvents = new ArrayDeque<>();
    queuedEvents = new ArrayDeque<>();
    // It's safe to use "this" because we don't send a message before exiting the constructor.
    @SuppressWarnings("nullness:methodref.receiver.bound")
    HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
    this.handler = handler;
  }

  /**
   * Copies the listener set.
   *
   * @param looper The new {@link Looper} for the copied listener set.
   * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
   *     sent during one {@link Looper} message queue iteration were handled by the listeners.
   * @return The copied listener set.
   */
  @CheckResult
  public ListenerSet<T> copy(Looper looper, IterationFinishedEvent<T> iterationFinishedEvent) {
    return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent);
  }

  /**
   * Adds a listener to the set.
   *
   * <p>If a listener is already present, it will not be added again.
   *
   * @param listener The listener to be added.
   */
  public void add(T listener) {
    if (released) {
      return;
    }
    Assertions.checkNotNull(listener);
    listeners.add(new ListenerHolder<>(listener));
  }

  /**
   * Removes a listener from the set.
   *
   * <p>If the listener is not present, nothing happens.
   *
   * @param listener The listener to be removed.
   */
  public void remove(T listener) {
    for (ListenerHolder<T> listenerHolder : listeners) {
      if (listenerHolder.listener.equals(listener)) {
        listenerHolder.release(iterationFinishedEvent);
        listeners.remove(listenerHolder);
      }
    }
  }

  /** Returns the number of added listeners. */
  public int size() {
    return listeners.size();
  }

  /**
   * Adds an event that is sent to the listeners when {@link #flushEvents} is called.
   *
   * @param eventFlag An integer indicating the type of the event, or {@link C#INDEX_UNSET} to
   *     report this event without flag.
   * @param event The event.
   */
  public void queueEvent(int eventFlag, Event<T> event) {
    CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
    queuedEvents.add(
        () -> {
          for (ListenerHolder<T> holder : listenerSnapshot) {
            holder.invoke(eventFlag, event);
          }
        });
  }

  /** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */
  public void flushEvents() {
    if (queuedEvents.isEmpty()) {
      return;
    }
    if (!handler.hasMessages(MSG_ITERATION_FINISHED)) {
      handler.sendMessageAtFrontOfQueue(handler.obtainMessage(MSG_ITERATION_FINISHED));
    }
    boolean recursiveFlushInProgress = !flushingEvents.isEmpty();
    flushingEvents.addAll(queuedEvents);
    queuedEvents.clear();
    if (recursiveFlushInProgress) {
      // Recursive call to flush. Let the outer call handle the flush queue.
      return;
    }
    while (!flushingEvents.isEmpty()) {
      flushingEvents.peekFirst().run();
      flushingEvents.removeFirst();
    }
  }

  /**
   * {@link #queueEvent(int, Event) Queues} a single event and immediately {@link #flushEvents()
   * flushes} the event queue to notify all listeners.
   *
   * @param eventFlag An integer flag indicating the type of the event, or {@link C#INDEX_UNSET} to
   *     report this event without flag.
   * @param event The event.
   */
  public void sendEvent(int eventFlag, Event<T> event) {
    queueEvent(eventFlag, event);
    flushEvents();
  }

  /**
   * Releases the set of listeners immediately.
   *
   * <p>This will ensure no events are sent to any listener after this method has been called.
   */
  public void release() {
    for (ListenerHolder<T> listenerHolder : listeners) {
      listenerHolder.release(iterationFinishedEvent);
    }
    listeners.clear();
    released = true;
  }

  private boolean handleMessage(Message message) {
    for (ListenerHolder<T> holder : listeners) {
      holder.iterationFinished(iterationFinishedEvent);
      if (handler.hasMessages(MSG_ITERATION_FINISHED)) {
        // The invocation above triggered new events (and thus scheduled a new message). We need
        // to stop here because this new message will take care of informing every listener about
        // the new update (including the ones already called here).
        break;
      }
    }
    return true;
  }

  private static final class ListenerHolder<T extends @NonNull Object> {

    public final T listener;

    private FlagSet.Builder flagsBuilder;
    private boolean needsIterationFinishedEvent;
    private boolean released;

    public ListenerHolder(T listener) {
      this.listener = listener;
      this.flagsBuilder = new FlagSet.Builder();
    }

    public void release(IterationFinishedEvent<T> event) {
      released = true;
      if (needsIterationFinishedEvent) {
        event.invoke(listener, flagsBuilder.build());
      }
    }

    public void invoke(int eventFlag, Event<T> event) {
      if (!released) {
        if (eventFlag != C.INDEX_UNSET) {
          flagsBuilder.add(eventFlag);
        }
        needsIterationFinishedEvent = true;
        event.invoke(listener);
      }
    }

    public void iterationFinished(IterationFinishedEvent<T> event) {
      if (!released && needsIterationFinishedEvent) {
        // Reset flags before invoking the listener to ensure we keep all new flags that are set by
        // recursive events triggered from this callback.
        FlagSet flagsToNotify = flagsBuilder.build();
        flagsBuilder = new FlagSet.Builder();
        needsIterationFinishedEvent = false;
        event.invoke(listener, flagsToNotify);
      }
    }

    @Override
    public boolean equals(@Nullable Object other) {
      if (this == other) {
        return true;
      }
      if (other == null || getClass() != other.getClass()) {
        return false;
      }
      return listener.equals(((ListenerHolder<?>) other).listener);
    }

    @Override
    public int hashCode() {
      return listener.hashCode();
    }
  }
}
