package org.itxtech.synapseapi.messaging;

import cn.nukkit.plugin.Plugin;
import cn.nukkit.utils.MainLogger;
import com.google.common.collect.ImmutableSet;
import org.itxtech.synapseapi.SynapseEntry;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class StandardMessenger implements Messenger {

    private final Map<String, Set<PluginMessageListenerRegistration>> incomingByChannel = new HashMap<>();
    private final Map<Plugin, Set<PluginMessageListenerRegistration>> incomingByPlugin = new HashMap<>();
    private final Map<String, Set<Plugin>> outgoingByChannel = new HashMap<>();
    private final Map<Plugin, Set<String>> outgoingByPlugin = new HashMap<>();
    private final Object incomingLock = new Object();
    private final Object outgoingLock = new Object();

    public static void validateChannel(String channel) {
        if (channel == null) {
            throw new IllegalArgumentException("Channel cannot be null");
        }
        if (channel.length() > 20) {
            throw new ChannelNameTooLongException(channel);
        }
    }

    public static void validatePluginMessage(Messenger messenger, Plugin source, String channel, byte[] message) {
        if (messenger == null) {
            throw new IllegalArgumentException("Messenger cannot be null");
        }
        if (source == null) {
            throw new IllegalArgumentException("Plugin source cannot be null");
        }
        if (!source.isEnabled()) {
            throw new IllegalArgumentException("Plugin must be enabled to send messages");
        }
        if (message == null) {
            throw new IllegalArgumentException("Message cannot be null");
        }
        if (!messenger.isOutgoingChannelRegistered(source, channel)) {
            throw new ChannelNotRegisteredException(channel);
        }
        if (message.length > 32766) {
            throw new MessageTooLargeException(message);
        }
        StandardMessenger.validateChannel(channel);
    }

    private void addToOutgoing(Plugin plugin, String channel) {
        synchronized (this.outgoingLock) {
            Set<Plugin> plugins = this.outgoingByChannel.get(channel);
            Set<String> channels = this.outgoingByPlugin.get(plugin);
            if (plugins == null) {
                plugins = new HashSet<>();
                this.outgoingByChannel.put(channel, plugins);
            }
            if (channels == null) {
                channels = new HashSet<>();
                this.outgoingByPlugin.put(plugin, channels);
            }
            plugins.add(plugin);
            channels.add(channel);
        }
    }

    private void removeFromOutgoing(Plugin plugin, String channel) {
        synchronized (this.outgoingLock) {
            Set<Plugin> plugins = this.outgoingByChannel.get(channel);
            Set<String> channels = this.outgoingByPlugin.get(plugin);
            if (plugins != null) {
                plugins.remove(plugin);
                if (plugins.isEmpty()) {
                    this.outgoingByChannel.remove(channel);
                }
            }
            if (channels != null) {
                channels.remove(channel);
                if (channels.isEmpty()) {
                    this.outgoingByChannel.remove(channel);
                }
            }
        }
    }

    private void removeFromOutgoing(Plugin plugin) {
        synchronized (this.outgoingLock) {
            Set<String> channels = this.outgoingByPlugin.get(plugin);
            if (channels != null) {
                String[] toRemove = channels.toArray(new String[0]);
                this.outgoingByPlugin.remove(plugin);

                int n = toRemove.length;
                int n2 = 0;
                while (n2 < n) {
                    String channel = toRemove[n2];
                    this.removeFromOutgoing(plugin, channel);
                    ++n2;
                }
            }
        }
    }

    private void addToIncoming(PluginMessageListenerRegistration registration) {
        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByChannel.get(registration.getChannel());
            if (registrations == null) {
                registrations = new HashSet<>();
                this.incomingByChannel.put(registration.getChannel(), registrations);
            } else if (registrations.contains(registration)) {
                throw new IllegalArgumentException("This registration already exists");
            }
            registrations.add(registration);
            registrations = this.incomingByPlugin.get(registration.getPlugin());
            if (registrations == null) {
                registrations = new HashSet<>();
                this.incomingByPlugin.put(registration.getPlugin(), registrations);
            } else if (registrations.contains(registration)) {
                throw new IllegalArgumentException("This registration already exists");
            }
            registrations.add(registration);
        }
    }

    private void removeFromIncoming(PluginMessageListenerRegistration registration) {
        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByChannel.get(registration.getChannel());
            if (registrations != null) {
                registrations.remove(registration);
                if (registrations.isEmpty()) {
                    this.incomingByChannel.remove(registration.getChannel());
                }
            }
            if ((registrations = this.incomingByPlugin.get(registration.getPlugin())) != null) {
                registrations.remove(registration);
                if (registrations.isEmpty()) {
                    this.incomingByPlugin.remove(registration.getPlugin());
                }
            }
        }
    }

    private void removeFromIncoming(Plugin plugin, String channel) {
        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                PluginMessageListenerRegistration[] toRemove = registrations.toArray(new PluginMessageListenerRegistration[0]);
                int n = toRemove.length;
                int n2 = 0;
                while (n2 < n) {
                    PluginMessageListenerRegistration registration = toRemove[n2];
                    if (registration.getChannel().equals(channel)) {
                        this.removeFromIncoming(registration);
                    }
                    ++n2;
                }
            }
        }
    }

    private void removeFromIncoming(Plugin plugin) {
        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                PluginMessageListenerRegistration[] toRemove = registrations.toArray(new PluginMessageListenerRegistration[0]);
                this.incomingByPlugin.remove(plugin);

                int n = toRemove.length;
                int n2 = 0;
                while (n2 < n) {
                    PluginMessageListenerRegistration registration = toRemove[n2];
                    this.removeFromIncoming(registration);
                    ++n2;
                }
            }
        }
    }

    @Override
    public boolean isReservedChannel(String channel) {
        StandardMessenger.validateChannel(channel);
        return false;
    }

    @Override
    public void registerOutgoingPluginChannel(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        if (this.isReservedChannel(channel)) {
            throw new ReservedChannelException(channel);
        }
        this.addToOutgoing(plugin, channel);
    }

    @Override
    public void unregisterOutgoingPluginChannel(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        this.removeFromOutgoing(plugin, channel);
    }

    @Override
    public void unregisterOutgoingPluginChannel(Plugin plugin) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        this.removeFromOutgoing(plugin);
    }

    @Override
    public PluginMessageListenerRegistration registerIncomingPluginChannel(Plugin plugin, String channel, PluginMessageListener listener) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        if (this.isReservedChannel(channel)) {
            throw new ReservedChannelException(channel);
        }
        if (listener == null) {
            throw new IllegalArgumentException("Listener cannot be null");
        }
        PluginMessageListenerRegistration result = new PluginMessageListenerRegistration(this, plugin, channel, listener);
        this.addToIncoming(result);
        return result;
    }

    @Override
    public void unregisterIncomingPluginChannel(Plugin plugin, String channel, PluginMessageListener listener) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        if (listener == null) {
            throw new IllegalArgumentException("Listener cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        this.removeFromIncoming(new PluginMessageListenerRegistration(this, plugin, channel, listener));
    }

    @Override
    public void unregisterIncomingPluginChannel(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        this.removeFromIncoming(plugin, channel);
    }

    @Override
    public void unregisterIncomingPluginChannel(Plugin plugin) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        this.removeFromIncoming(plugin);
    }

    @Override
    public Set<String> getOutgoingChannels() {
        synchronized (this.outgoingLock) {
            Set<String> keys = this.outgoingByChannel.keySet();
            return ImmutableSet.copyOf(keys);
        }
    }

    @Override
    public Set<String> getOutgoingChannels(Plugin plugin) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }

        synchronized (this.outgoingLock) {
            Set<String> channels = this.outgoingByPlugin.get(plugin);
            if (channels != null) {
                return ImmutableSet.copyOf(channels);
            }
            return ImmutableSet.of();
        }
    }

    @Override
    public Set<String> getIncomingChannels() {
        synchronized (this.incomingLock) {
            Set<String> keys = this.incomingByChannel.keySet();
            return ImmutableSet.copyOf(keys);
        }
    }

    @Override
    public Set<String> getIncomingChannels(Plugin plugin) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                ImmutableSet.Builder builder = ImmutableSet.builder();
                for (PluginMessageListenerRegistration registration : registrations) {
                    builder.add(registration.getChannel());
                }
                return builder.build();
            }
            return ImmutableSet.of();
        }
    }

    @Override
    public Set<PluginMessageListenerRegistration> getIncomingChannelRegistrations(Plugin plugin) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                return ImmutableSet.copyOf(registrations);
            }
            return ImmutableSet.of();
        }
    }

    @Override
    public Set<PluginMessageListenerRegistration> getIncomingChannelRegistrations(String channel) {
        StandardMessenger.validateChannel(channel);

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByChannel.get(channel);
            if (registrations != null) {
                return ImmutableSet.copyOf(registrations);
            }
            return ImmutableSet.of();
        }
    }

    @Override
    public Set<PluginMessageListenerRegistration> getIncomingChannelRegistrations(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                ImmutableSet.Builder builder = ImmutableSet.builder();
                for (PluginMessageListenerRegistration registration : registrations) {
                    if (!registration.getChannel().equals(channel)) continue;
                    builder.add(registration);
                }
                return builder.build();
            }
            return ImmutableSet.of();
        }
    }

    @Override
    public boolean isRegistrationValid(PluginMessageListenerRegistration registration) {
        if (registration == null) {
            throw new IllegalArgumentException("Registration cannot be null");
        }

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(registration.getPlugin());

            return registrations != null && registrations.contains(registration);
        }
    }

    @Override
    public boolean isIncomingChannelRegistered(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);

        synchronized (this.incomingLock) {
            Set<PluginMessageListenerRegistration> registrations = this.incomingByPlugin.get(plugin);
            if (registrations != null) {
                for (PluginMessageListenerRegistration registration : registrations) {
                    if (!registration.getChannel().equals(channel)) continue;
                    return true;
                }
            }
            return false;
        }
    }

    @Override
    public boolean isOutgoingChannelRegistered(Plugin plugin, String channel) {
        if (plugin == null) {
            throw new IllegalArgumentException("Plugin cannot be null");
        }
        StandardMessenger.validateChannel(channel);

        synchronized (this.outgoingLock) {
            Set<String> channels = this.outgoingByPlugin.get(plugin);
            return channels != null && channels.contains(channel);
        }
    }

    @Override
    public void dispatchIncomingMessage(SynapseEntry entry, String channel, byte[] message) {
        if (message == null) {
            throw new IllegalArgumentException("Message cannot be null");
        }
        StandardMessenger.validateChannel(channel);
        Set<PluginMessageListenerRegistration> registrations = this.getIncomingChannelRegistrations(channel);
        for (PluginMessageListenerRegistration registration : registrations) {
            try {
                registration.getListener().onPluginMessageReceived(entry, channel, message);
            } catch (Throwable t) {
                MainLogger.getLogger().warning("Could not pass incoming plugin message to " + registration.getPlugin(), t);
            }
        }
    }
}