/*
 * This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
 * Copyright (C) 2016-2023 ViaVersion and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.viaversion.viaversion.bukkit.platform;

import com.google.common.base.Preconditions;
import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer;
import com.viaversion.viaversion.bukkit.util.NMSUtil;
import com.viaversion.viaversion.platform.LegacyViaInjector;
import com.viaversion.viaversion.platform.WrappedChannelInitializer;
import com.viaversion.viaversion.util.ReflectionUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginDescriptionFile;
import org.checkerframework.checker.nullness.qual.Nullable;

public class BukkitViaInjector extends LegacyViaInjector {

    private static final boolean HAS_WORLD_VERSION_PROTOCOL_VERSION = PaperViaInjector.hasClass("net.minecraft.SharedConstants")
            && PaperViaInjector.hasClass("net.minecraft.WorldVersion")
            && !PaperViaInjector.hasClass("com.mojang.bridge.game.GameVersion");

    @Override
    public void inject() throws ReflectiveOperationException {
        if (PaperViaInjector.PAPER_INJECTION_METHOD) {
            PaperViaInjector.setPaperChannelInitializeListener();
            return;
        }

        super.inject();
    }

    @Override
    public void uninject() throws ReflectiveOperationException {
        if (PaperViaInjector.PAPER_INJECTION_METHOD) {
            PaperViaInjector.removePaperChannelInitializeListener();
            return;
        }

        super.uninject();
    }

    @Override
    public int getServerProtocolVersion() throws ReflectiveOperationException {
        if (PaperViaInjector.PAPER_PROTOCOL_METHOD) {
            //noinspection deprecation
            return Bukkit.getUnsafe().getProtocolVersion();
        }

        return HAS_WORLD_VERSION_PROTOCOL_VERSION ? cursedProtocolDetection() : veryCursedProtocolDetection();
    }

    private int cursedProtocolDetection() throws ReflectiveOperationException {
        // Get the version from SharedConstants.getWorldVersion().getProtocolVersion()
        Class<?> sharedConstantsClass = Class.forName("net.minecraft.SharedConstants");
        Class<?> worldVersionClass = Class.forName("net.minecraft.WorldVersion");
        Method getWorldVersionMethod = null;
        for (Method method : sharedConstantsClass.getDeclaredMethods()) {
            if (method.getReturnType() == worldVersionClass && method.getParameterTypes().length == 0) {
                getWorldVersionMethod = method;
                break;
            }
        }
        Preconditions.checkNotNull(getWorldVersionMethod, "Failed to get world version method");

        Object worldVersion = getWorldVersionMethod.invoke(null);
        for (Method method : worldVersionClass.getDeclaredMethods()) {
            if (method.getReturnType() == int.class && method.getParameterTypes().length == 0) {
                return (int) method.invoke(worldVersion);
            }
        }
        throw new IllegalAccessException("Failed to find protocol version method in WorldVersion");
    }

    private int veryCursedProtocolDetection() throws ReflectiveOperationException {
        // Time to go on a journey! The protocol version is hidden inside an int in ServerPing.ServerData, that is only set once the server has ticked once
        // Grab a static instance of the server
        Class<?> serverClazz = NMSUtil.nms("MinecraftServer", "net.minecraft.server.MinecraftServer");
        Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer");
        Preconditions.checkNotNull(server, "Failed to get server instance");

        // Grab the ping class and find the field to access it
        Class<?> pingClazz = NMSUtil.nms(
                "ServerPing",
                "net.minecraft.network.protocol.status.ServerPing"
        );
        Object ping = null;
        for (Field field : serverClazz.getDeclaredFields()) {
            if (field.getType() == pingClazz) {
                field.setAccessible(true);
                ping = field.get(server);
                break;
            }
        }
        Preconditions.checkNotNull(ping, "Failed to get server ping");

        // Now get the ServerData inside ServerPing
        Class<?> serverDataClass = NMSUtil.nms(
                "ServerPing$ServerData",
                "net.minecraft.network.protocol.status.ServerPing$ServerData"
        );
        Object serverData = null;
        for (Field field : pingClazz.getDeclaredFields()) {
            if (field.getType() == serverDataClass) {
                field.setAccessible(true);
                serverData = field.get(ping);
                break;
            }
        }
        Preconditions.checkNotNull(serverData, "Failed to get server data");

        // Get protocol version field
        for (Field field : serverDataClass.getDeclaredFields()) {
            if (field.getType() != int.class) {
                continue;
            }

            field.setAccessible(true);
            int protocolVersion = (int) field.get(serverData);
            if (protocolVersion != -1) {
                return protocolVersion;
            }
        }
        throw new RuntimeException("Failed to get server");
    }

    @Override
    protected @Nullable Object getServerConnection() throws ReflectiveOperationException {
        Class<?> serverClass = NMSUtil.nms(
                "MinecraftServer",
                "net.minecraft.server.MinecraftServer"
        );
        Class<?> connectionClass = NMSUtil.nms(
                "ServerConnection",
                "net.minecraft.server.network.ServerConnection"
        );

        Object server = ReflectionUtil.invokeStatic(serverClass, "getServer");
        for (Method method : serverClass.getDeclaredMethods()) {
            if (method.getReturnType() != connectionClass || method.getParameterTypes().length != 0) {
                continue;
            }

            // We need the method that initiates the connection if not yet set
            Object connection = method.invoke(server);
            if (connection != null) {
                return connection;
            }
        }
        return null;
    }

    @Override
    protected WrappedChannelInitializer createChannelInitializer(ChannelInitializer<Channel> oldInitializer) {
        return new BukkitChannelInitializer(oldInitializer);
    }

    @Override
    protected void blame(ChannelHandler bootstrapAcceptor) throws ReflectiveOperationException {
        // Let's find who to blame!
        ClassLoader classLoader = bootstrapAcceptor.getClass().getClassLoader();
        if (classLoader.getClass().getName().equals("org.bukkit.plugin.java.PluginClassLoader")) {
            PluginDescriptionFile description = ReflectionUtil.get(classLoader, "description", PluginDescriptionFile.class);
            throw new RuntimeException("Unable to inject, due to " + bootstrapAcceptor.getClass().getName() + ", try without the plugin " + description.getName() + "?");
        } else {
            throw new RuntimeException("Unable to find core component 'childHandler', please check your plugins. issue: " + bootstrapAcceptor.getClass().getName());
        }
    }

    @Override
    public boolean lateProtocolVersionSetting() {
        return !(PaperViaInjector.PAPER_PROTOCOL_METHOD || HAS_WORLD_VERSION_PROTOCOL_VERSION);
    }

    public boolean isBinded() {
        if (PaperViaInjector.PAPER_INJECTION_METHOD) {
            return true;
        }
        try {
            Object connection = getServerConnection();
            if (connection == null) {
                return false;
            }

            for (Field field : connection.getClass().getDeclaredFields()) {
                if (!List.class.isAssignableFrom(field.getType())) {
                    continue;
                }

                field.setAccessible(true);
                List<?> value = (List<?>) field.get(connection);
                // Check if the list has at least one element
                synchronized (value) {
                    if (!value.isEmpty() && value.get(0) instanceof ChannelFuture) {
                        return true;
                    }
                }
            }
        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
        return false;
    }
}