package org.allaymc.server;

import io.netty.util.ResourceLeakDetector;
import io.sentry.Sentry;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.extern.slf4j.Slf4j;
import org.allaymc.api.AllayAPI;
import org.allaymc.api.block.type.BlockType;
import org.allaymc.api.blockentity.type.BlockEntityType;
import org.allaymc.api.command.selector.EntitySelectorAPI;
import org.allaymc.api.command.tree.CommandNodeFactory;
import org.allaymc.api.command.tree.CommandTree;
import org.allaymc.api.container.FakeContainerFactory;
import org.allaymc.api.entity.effect.EffectType;
import org.allaymc.api.entity.type.EntityType;
import org.allaymc.api.eventbus.EventBus;
import org.allaymc.api.item.enchantment.EnchantmentType;
import org.allaymc.api.item.type.ItemType;
import org.allaymc.api.message.I18n;
import org.allaymc.api.message.TrKeys;
import org.allaymc.api.registry.DoubleKeyMappedRegistry;
import org.allaymc.api.registry.Registries;
import org.allaymc.api.registry.SimpleMappedRegistry;
import org.allaymc.api.scheduler.Scheduler;
import org.allaymc.api.server.Server;
import org.allaymc.api.utils.NBTIO;
import org.allaymc.api.utils.identifier.Identifier;
import org.allaymc.api.world.biome.BiomeType;
import org.allaymc.api.world.dimension.DimensionType;
import org.allaymc.api.world.feature.WorldFeature;
import org.allaymc.server.command.selector.AllayEntitySelectorAPI;
import org.allaymc.server.command.tree.AllayCommandNodeFactory;
import org.allaymc.server.command.tree.AllayCommandTree;
import org.allaymc.server.container.AllayFakeContainerFactory;
import org.allaymc.server.eventbus.AllayEventBus;
import org.allaymc.server.extension.ExtensionManager;
import org.allaymc.server.gui.Dashboard;
import org.allaymc.server.item.creative.AllayCreativeItemRegistry;
import org.allaymc.server.message.AllayI18n;
import org.allaymc.server.message.AllayI18nLoader;
import org.allaymc.server.network.ProtocolInfo;
import org.allaymc.server.pdc.AllayPersistentDataTypeRegistry;
import org.allaymc.server.registry.AllayCommandRegistry;
import org.allaymc.server.registry.InternalRegistries;
import org.allaymc.server.registry.loader.*;
import org.allaymc.server.registry.populator.*;
import org.allaymc.server.scheduler.AllayScheduler;
import org.allaymc.server.utils.AllayNBTIO;
import org.allaymc.server.utils.DynamicURLClassLoader;
import org.allaymc.server.utils.GitProperties;
import org.allaymc.server.world.biome.CustomBiomeIdAllocator;
import org.allaymc.server.world.dimension.CustomDimensionIdAllocator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.async.AsyncLoggerContextSelector;
import org.jetbrains.annotations.VisibleForTesting;

import java.nio.file.Path;
import java.util.HashMap;

import static java.lang.Thread.sleep;

/**
 * @author daoge_cmd
 */
@Slf4j
public final class Allay {

    public static final DynamicURLClassLoader EXTRA_RESOURCE_CLASS_LOADER = new DynamicURLClassLoader(Allay.class.getClassLoader());
    public static final ExtensionManager EXTENSION_MANAGER = new ExtensionManager(Path.of("extensions"));

    public static Dashboard DASHBOARD;
    private static boolean initialized;

    public static void main(String[] args) throws InterruptedException {
        long initialTime = System.currentTimeMillis();
        if (GitProperties.isDevBuild() && !AllayServer.getSettings().genericSettings().forceEnableSentry()) {
            // Enable sentry only in non-dev build
            Sentry.close();
        }
        ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED);
        // Disable scientific notation in joml
        System.setProperty("joml.format", "false");
        // Enable async logging
        System.setProperty("log4j2.contextSelector", AsyncLoggerContextSelector.class.getName());
        System.setProperty("bedrock.maxDecompressedBytes", String.valueOf(AllayServer.getSettings().networkSettings().maxDecompressedBytes()));

        initI18n();
        EXTENSION_MANAGER.loadExtensions(args);

        // Check if the environment is headless
        if (isHeadless()) {
            AllayServer.getSettings().genericSettings().enableGui(false);
        }
        if (AllayServer.getSettings().genericSettings().enableGui()) {
            try {
                DASHBOARD = Dashboard.getInstance();
            } catch (Throwable t) {
                log.error("Cannot init Dashboard!", t);
                AllayServer.getSettings().genericSettings().enableGui(false);
            }
        }

        log.info(I18n.get().tr(TrKeys.ALLAY_SERVER_STARTING));

        try {
            initAllay();
        } catch (Exception e) {
            log.error("Cannot init Allay API!", e);
            if (DASHBOARD != null) {
                sleep(5000);
            }
            LogManager.shutdown();
            System.exit(1);
        }

        try {
            ((AllayServer) Server.getInstance()).start(initialTime);
        } catch (Throwable t) {
            log.error("Error while starting the server!", t);
            // The server may not be initialized correctly, so we can't call Server::shutdown()
            // to stop the server
            if (DASHBOARD != null) {
                sleep(5000);
            }
            LogManager.shutdown();
            System.exit(1);
        }

        log.info(I18n.get().tr(TrKeys.ALLAY_SERVER_STOPPED));
        // Server has been shutdown
        // Call System.exit(0) to stop other non-daemon threads
        LogManager.shutdown();
        System.exit(0);
    }

    private static boolean isHeadless() {
        try {
            var graphicsEnv = Class.forName("java.awt.GraphicsEnvironment");
            var isHeadless = graphicsEnv.getDeclaredMethod("isHeadless");
            return (boolean) isHeadless.invoke(null);
        } catch (Exception ignore) {
        }
        return true;
    }

    /**
     * Initializes API bindings and registries once.
     *
     * <p>{@link #initI18n()} must run first because registry initialization may emit translated output.</p>
     */
    @VisibleForTesting
    public static synchronized void initAllay() {
        if (initialized) {
            return;
        }
        initAllayAPI();
        initRegistries();
        initialized = true;
    }

    private static void initAllayAPI() {
        var api = AllayAPI.getInstance();
        if (api.isImplemented()) return;

        // Common
        api.bind(Server.class, AllayServer::getInstance);
        api.bind(Scheduler.Factory.class, () -> AllayScheduler::new);
        api.bind(EventBus.Factory.class, () -> AllayEventBus::new);

        // Command
        api.bind(EntitySelectorAPI.class, AllayEntitySelectorAPI::new);
        api.bind(CommandTree.Factory.class, () -> AllayCommandTree::create);
        api.bind(CommandNodeFactory.class, AllayCommandNodeFactory::new);

        // Misc
        api.bind(NBTIO.class, AllayNBTIO::new);
        api.bind(FakeContainerFactory.class, AllayFakeContainerFactory::new);

        api.implement("allay", GitProperties.isDevBuild());

        var versionStr = ProtocolInfo.getLowestCodec().getMinecraftVersion() + " - " + ProtocolInfo.getLatestCodec().getMinecraftVersion();
        var protocolStr = ProtocolInfo.getLowestCodec().getProtocolVersion() + " - " + ProtocolInfo.getLatestCodec().getProtocolVersion();
        log.info(I18n.get().tr(
                TrKeys.ALLAY_COMMAND_VERSION_OUTPUT,
                AllayAPI.getInstance().getCoreName(), GitProperties.getBuildVersion(),
                GitProperties.getBuildApiVersion(), versionStr, protocolStr
        ));
        if (AllayAPI.getInstance().isDevBuild()) {
            log.warn(I18n.get().tr(TrKeys.ALLAY_SERVER_IS_DEV_VERSION));
        }
    }

    private static void initRegistries() {
        // Item
        DoubleKeyMappedRegistry.create(
                RegistryLoaders.empty(() -> new DoubleKeyMappedRegistry.MapPair<>(new Int2ObjectOpenHashMap<>(), new HashMap<Identifier, EnchantmentType>())),
                r -> Registries.ENCHANTMENTS = r,
                new EnchantmentTypeRegistryPopulator()
        );
        InternalRegistries.ITEM_DATA = SimpleMappedRegistry.create(new ItemDataRegistryLoader());
        InternalRegistries.ITEM_TAGS = SimpleMappedRegistry.create(new ItemTagRegistryLoader());
        InternalRegistries.VANILLA_ITEM_DEFINITIONS = SimpleMappedRegistry.create(new VanillaItemDefinitionRegistryLoader());
        SimpleMappedRegistry.create(
                RegistryLoaders.empty(() -> new HashMap<Identifier, ItemType<?>>()),
                r -> Registries.ITEMS = r,
                new ItemTypeRegistryPopulator()
        );
        Registries.COMPOSTABLE_ITEMS = SimpleMappedRegistry.create(new CompostableItemRegistryLoader());

        // Fishing
        Registries.FISHING_LOOTS = SimpleMappedRegistry.create(new FishingLootRegistryLoader());

        // BlockEntity
        SimpleMappedRegistry.create(
                RegistryLoaders.empty(() -> new HashMap<String, BlockEntityType<?>>()),
                r -> Registries.BLOCK_ENTITIES = r,
                new BlockEntityTypeRegistryPopulator()
        );

        // Block
        InternalRegistries.BLOCK_STATE_DATA = SimpleMappedRegistry.create(new BlockStateDataRegistryLoader());
        InternalRegistries.BLOCK_TAGS = SimpleMappedRegistry.create(new BlockTagRegistryLoader());
        InternalRegistries.BLOCK_DEFAULT_STATE_HASHES = SimpleMappedRegistry.create(new BlockDefaultStateHashRegistryLoader());
        InternalRegistries.BLOCK_PROPERTY_PROCESSORS = SimpleMappedRegistry.create(new BlockPropertyProcessorRegistryLoader());
        Registries.BLOCK_STATE_PALETTE = SimpleMappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
        SimpleMappedRegistry.create(
                RegistryLoaders.empty(() -> new HashMap<Identifier, BlockType<?>>()),
                r -> Registries.BLOCKS = r,
                new BlockTypeRegistryPopulator()
        );

        // Entity
        DoubleKeyMappedRegistry.create(
                RegistryLoaders.empty(() -> new DoubleKeyMappedRegistry.MapPair<>(new Int2ObjectOpenHashMap<>(), new HashMap<Identifier, EffectType>())),
                r -> Registries.EFFECTS = r,
                new EffectTypeRegistryPopulator()
        );
        SimpleMappedRegistry.create(
                RegistryLoaders.empty(() -> new HashMap<Identifier, EntityType<?>>()),
                r -> Registries.ENTITIES = r,
                new EntityTypeRegistryPopulator()
        );

        // Biome
        InternalRegistries.BIOME_DATA = SimpleMappedRegistry.create(new BiomeDataRegistryLoader());
        DoubleKeyMappedRegistry.create(
                RegistryLoaders.empty(() -> new DoubleKeyMappedRegistry.MapPair<>(new Int2ObjectOpenHashMap<>(), new HashMap<Identifier, BiomeType>())),
                r -> Registries.BIOMES = r,
                new BiomeTypeRegistryPopulator()
        );
        CustomBiomeIdAllocator.init();
        DoubleKeyMappedRegistry.create(
                RegistryLoaders.empty(() -> new DoubleKeyMappedRegistry.MapPair<>(new Int2ObjectOpenHashMap<>(), new HashMap<Identifier, DimensionType>())),
                r -> Registries.DIMENSIONS = r,
                new DimensionTypeRegistryPopulator()
        );
        CustomDimensionIdAllocator.init();

        // World
        Registries.WORLD_STORAGE_FACTORIES = SimpleMappedRegistry.create(new WorldStorageFactoryRegistryLoader());
        Registries.WORLD_GENERATOR_FACTORIES = SimpleMappedRegistry.create(new WorldGeneratorFactoryRegistryLoader());
        SimpleMappedRegistry.create(
                RegistryLoaders.empty(() -> new HashMap<Identifier, WorldFeature>()),
                r -> Registries.WORLD_FEATURES = r,
                new WorldFeatureRegistryPopulator()
        );

        // POI (Point of Interest)
        Registries.POI_TYPES = SimpleMappedRegistry.create(new PoiTypeRegistryLoader());

        // Creative Item
        Registries.CREATIVE_ITEMS = new AllayCreativeItemRegistry();

        // Recipe
        Registries.RECIPES = SimpleMappedRegistry.create(new RecipeRegistryLoader());

        // Trim Data
        InternalRegistries.TRIM_PATTERNS = SimpleMappedRegistry.create(new TrimPatternRegistryLoader());
        InternalRegistries.TRIM_MATERIALS = SimpleMappedRegistry.create(new TrimMaterialRegistryLoader());

        // Pack
        Registries.PACKS = SimpleMappedRegistry.create(new PackRegistryLoader());

        // Command
        Registries.COMMANDS = new AllayCommandRegistry();

        // Persistent Data Container (PDC)
        Registries.PERSISTENT_DATA_TYPES = new AllayPersistentDataTypeRegistry();

        // Dispenser
        Registries.DISPENSER_BEHAVIORS = SimpleMappedRegistry.create(new DispenserBehaviorRegistryLoader());
    }

    @VisibleForTesting
    public static void initI18n() {
        if (I18n.get() != null) {
            return;
        }

        AllayAPI.getInstance().bindI18n(new AllayI18n(new AllayI18nLoader(), AllayServer.getSettings().genericSettings().language()));
    }
}
