package io.github.hapjava.server.impl.connections;

import io.github.hapjava.characteristics.EventableCharacteristic;
import io.github.hapjava.server.impl.http.HomekitClientConnection;
import io.github.hapjava.server.impl.http.HttpResponse;
import io.github.hapjava.server.impl.json.EventController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SubscriptionManager {

  private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionManager.class);

  private final ConcurrentMap<EventableCharacteristic, Set<HomekitClientConnection>> subscriptions =
      new ConcurrentHashMap<>();
  private final ConcurrentMap<HomekitClientConnection, Set<EventableCharacteristic>> reverse =
      new ConcurrentHashMap<>();
  private final ConcurrentMap<HomekitClientConnection, ArrayList<PendingNotification>>
      pendingNotifications = new ConcurrentHashMap<>();
  private int nestedBatches = 0;

  public synchronized void addSubscription(
      int aid,
      int iid,
      EventableCharacteristic characteristic,
      HomekitClientConnection connection) {
    synchronized (this) {
      if (!subscriptions.containsKey(characteristic)) {
        subscriptions.putIfAbsent(characteristic, newSet());
      }
      subscriptions.get(characteristic).add(connection);
      if (subscriptions.get(characteristic).size() == 1) {
        characteristic.subscribe(
            () -> {
              publish(aid, iid, characteristic);
            });
      }

      if (!reverse.containsKey(connection)) {
        reverse.putIfAbsent(connection, newSet());
      }
      reverse.get(connection).add(characteristic);
      LOGGER.trace(
          "Added subscription to " + characteristic.getClass() + " for " + connection.hashCode());
    }
  }

  public synchronized void removeSubscription(
      EventableCharacteristic characteristic, HomekitClientConnection connection) {
    Set<HomekitClientConnection> subscriptions = this.subscriptions.get(characteristic);
    if (subscriptions != null) {
      subscriptions.remove(connection);
      if (subscriptions.size() == 0) {
        characteristic.unsubscribe();
      }
    }

    Set<EventableCharacteristic> reverse = this.reverse.get(connection);
    if (reverse != null) {
      reverse.remove(characteristic);
    }
    LOGGER.trace(
        "Removed subscription to " + characteristic.getClass() + " for " + connection.hashCode());
  }

  public synchronized void removeConnection(HomekitClientConnection connection) {
    Set<EventableCharacteristic> characteristics = reverse.remove(connection);
    pendingNotifications.remove(connection);
    if (characteristics != null) {
      for (EventableCharacteristic characteristic : characteristics) {
        Set<HomekitClientConnection> characteristicSubscriptions =
            subscriptions.get(characteristic);
        characteristicSubscriptions.remove(connection);
        if (characteristicSubscriptions.isEmpty()) {
          LOGGER.trace("Unsubscribing from characteristic as all subscriptions are closed");
          characteristic.unsubscribe();
          subscriptions.remove(characteristic);
        }
      }
    }
    LOGGER.trace("Removed connection {}", connection.hashCode());
  }

  private <T> Set<T> newSet() {
    return Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
  }

  public synchronized void batchUpdate() {
    ++this.nestedBatches;
  }

  public synchronized void completeUpdateBatch() {
    if (--this.nestedBatches == 0 && !pendingNotifications.isEmpty()) {
      LOGGER.trace("Publishing batched changes");
      for (ConcurrentMap.Entry<HomekitClientConnection, ArrayList<PendingNotification>> entry :
          pendingNotifications.entrySet()) {
        try {
          HttpResponse message = new EventController().getMessage(entry.getValue());
          entry.getKey().outOfBand(message);
        } catch (Exception e) {
          LOGGER.warn("Failed to create new event message", e);
        }
      }
      pendingNotifications.clear();
    }
  }

  public synchronized void publish(int accessoryId, int iid, EventableCharacteristic changed) {
    final Set<HomekitClientConnection> subscribers = subscriptions.get(changed);
    if ((subscribers == null) || (subscribers.isEmpty())) {
      LOGGER.debug("No subscribers to characteristic {} at accessory {} ", changed, accessoryId);
      return; // no subscribers
    }
    if (nestedBatches != 0) {
      LOGGER.trace("Batching change for accessory {} and characteristic {} " + accessoryId, iid);
      PendingNotification notification = new PendingNotification(accessoryId, iid, changed);
      for (HomekitClientConnection connection : subscribers) {
        if (!pendingNotifications.containsKey(connection)) {
          pendingNotifications.put(connection, new ArrayList<PendingNotification>());
        }
        pendingNotifications.get(connection).add(notification);
      }
      return;
    }

    try {
      HttpResponse message = new EventController().getMessage(accessoryId, iid, changed);
      LOGGER.trace("Publishing change for " + accessoryId);
      for (HomekitClientConnection connection : subscribers) {
        connection.outOfBand(message);
      }
    } catch (Exception e) {
      LOGGER.warn("Failed to create new event message", e);
    }
  }

  /** Remove all existing subscriptions */
  public void removeAll() {
    LOGGER.trace("Removing {} reverse connections from subscription manager", reverse.size());
    Iterator<HomekitClientConnection> i = reverse.keySet().iterator();
    while (i.hasNext()) {
      HomekitClientConnection connection = i.next();
      LOGGER.trace("Removing connection {}", connection.hashCode());
      removeConnection(connection);
    }
    LOGGER.trace("Subscription sizes are {} and {}", reverse.size(), subscriptions.size());
  }
}