/*
 * This file is part of Araknemu.
 *
 * Araknemu is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Araknemu 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
 *
 * Copyright (c) 2017-2019 Vincent Quatrevieux
 */

package fr.quatrevieux.araknemu.game;

import fr.arakne.utils.value.Colors;
import fr.arakne.utils.value.constant.Gender;
import fr.arakne.utils.value.constant.Race;
import fr.arakne.utils.value.helper.RandomUtil;
import fr.quatrevieux.araknemu.Araknemu;
import fr.quatrevieux.araknemu.DatabaseTestCase;
import fr.quatrevieux.araknemu.common.account.Permission;
import fr.quatrevieux.araknemu.common.session.SessionLogService;
import fr.quatrevieux.araknemu.core.config.Configuration;
import fr.quatrevieux.araknemu.core.config.DefaultConfiguration;
import fr.quatrevieux.araknemu.core.config.IniDriver;
import fr.quatrevieux.araknemu.core.dbal.DatabaseConfiguration;
import fr.quatrevieux.araknemu.core.dbal.DefaultDatabaseHandler;
import fr.quatrevieux.araknemu.core.dbal.executor.ConnectionPoolExecutor;
import fr.quatrevieux.araknemu.core.di.Container;
import fr.quatrevieux.araknemu.core.di.ContainerConfigurator;
import fr.quatrevieux.araknemu.core.di.ContainerException;
import fr.quatrevieux.araknemu.core.di.ContainerModule;
import fr.quatrevieux.araknemu.core.di.ItemPoolContainer;
import fr.quatrevieux.araknemu.core.network.Server;
import fr.quatrevieux.araknemu.core.network.parser.Dispatcher;
import fr.quatrevieux.araknemu.core.network.parser.Packet;
import fr.quatrevieux.araknemu.core.network.session.SessionFactory;
import fr.quatrevieux.araknemu.core.network.util.DummyChannel;
import fr.quatrevieux.araknemu.core.network.util.DummyServer;
import fr.quatrevieux.araknemu.data.constant.Characteristic;
import fr.quatrevieux.araknemu.data.living.entity.BanIp;
import fr.quatrevieux.araknemu.data.living.entity.account.Account;
import fr.quatrevieux.araknemu.data.living.entity.account.AccountBank;
import fr.quatrevieux.araknemu.data.living.entity.account.Banishment;
import fr.quatrevieux.araknemu.data.living.entity.account.BankItem;
import fr.quatrevieux.araknemu.data.living.entity.account.ConnectionLog;
import fr.quatrevieux.araknemu.data.living.entity.player.Player;
import fr.quatrevieux.araknemu.data.living.entity.player.PlayerItem;
import fr.quatrevieux.araknemu.data.living.entity.player.PlayerSpell;
import fr.quatrevieux.araknemu.data.living.repository.BanIpRepository;
import fr.quatrevieux.araknemu.data.living.repository.account.AccountBankRepository;
import fr.quatrevieux.araknemu.data.living.repository.account.AccountRepository;
import fr.quatrevieux.araknemu.data.living.repository.account.BanishmentRepository;
import fr.quatrevieux.araknemu.data.living.repository.account.BankItemRepository;
import fr.quatrevieux.araknemu.data.living.repository.account.ConnectionLogRepository;
import fr.quatrevieux.araknemu.data.living.repository.implementation.sql.SqlLivingRepositoriesModule;
import fr.quatrevieux.araknemu.data.living.repository.player.PlayerItemRepository;
import fr.quatrevieux.araknemu.data.living.repository.player.PlayerRepository;
import fr.quatrevieux.araknemu.data.living.repository.player.PlayerSpellRepository;
import fr.quatrevieux.araknemu.data.value.Position;
import fr.quatrevieux.araknemu.data.world.entity.SpellTemplate;
import fr.quatrevieux.araknemu.data.world.entity.character.PlayerExperience;
import fr.quatrevieux.araknemu.data.world.entity.character.PlayerRace;
import fr.quatrevieux.araknemu.data.world.entity.environment.MapTemplate;
import fr.quatrevieux.araknemu.data.world.entity.environment.MapTrigger;
import fr.quatrevieux.araknemu.data.world.entity.environment.area.Area;
import fr.quatrevieux.araknemu.data.world.entity.environment.area.SubArea;
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.Npc;
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.NpcExchange;
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.NpcTemplate;
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.Question;
import fr.quatrevieux.araknemu.data.world.entity.environment.npc.ResponseAction;
import fr.quatrevieux.araknemu.data.world.entity.item.ItemSet;
import fr.quatrevieux.araknemu.data.world.entity.item.ItemTemplate;
import fr.quatrevieux.araknemu.data.world.entity.item.ItemType;
import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterGroupData;
import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterGroupPosition;
import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterRewardData;
import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterRewardItem;
import fr.quatrevieux.araknemu.data.world.entity.monster.MonsterTemplate;
import fr.quatrevieux.araknemu.data.world.repository.SpellTemplateRepository;
import fr.quatrevieux.araknemu.data.world.repository.character.PlayerExperienceRepository;
import fr.quatrevieux.araknemu.data.world.repository.character.PlayerRaceRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.MapTemplateRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.MapTriggerRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.area.AreaRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.area.SubAreaRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.NpcExchangeRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.NpcRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.NpcTemplateRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.QuestionRepository;
import fr.quatrevieux.araknemu.data.world.repository.environment.npc.ResponseActionRepository;
import fr.quatrevieux.araknemu.data.world.repository.implementation.sql.SqlWorldRepositoriesModule;
import fr.quatrevieux.araknemu.data.world.repository.item.ItemSetRepository;
import fr.quatrevieux.araknemu.data.world.repository.item.ItemTemplateRepository;
import fr.quatrevieux.araknemu.data.world.repository.item.ItemTypeRepository;
import fr.quatrevieux.araknemu.data.world.repository.monster.MonsterGroupDataRepository;
import fr.quatrevieux.araknemu.data.world.repository.monster.MonsterGroupPositionRepository;
import fr.quatrevieux.araknemu.data.world.repository.monster.MonsterRewardItemRepository;
import fr.quatrevieux.araknemu.data.world.repository.monster.MonsterRewardRepository;
import fr.quatrevieux.araknemu.data.world.repository.monster.MonsterTemplateRepository;
import fr.quatrevieux.araknemu.game.account.AccountService;
import fr.quatrevieux.araknemu.game.account.GameAccount;
import fr.quatrevieux.araknemu.game.admin.AdminModule;
import fr.quatrevieux.araknemu.game.chat.ChannelType;
import fr.quatrevieux.araknemu.game.connector.RealmConnector;
import fr.quatrevieux.araknemu.game.exploration.ExplorationPlayer;
import fr.quatrevieux.araknemu.game.exploration.ExplorationService;
import fr.quatrevieux.araknemu.game.exploration.event.StartExploration;
import fr.quatrevieux.araknemu.game.player.GamePlayer;
import fr.quatrevieux.araknemu.game.player.PlayerService;
import fr.quatrevieux.araknemu.game.player.experience.PlayerExperienceService;
import fr.quatrevieux.araknemu.game.player.inventory.InventoryService;
import fr.quatrevieux.araknemu.game.player.race.PlayerRaceService;
import fr.quatrevieux.araknemu.game.player.spell.SpellBookService;
import fr.quatrevieux.araknemu.game.world.creature.characteristics.DefaultCharacteristics;
import fr.quatrevieux.araknemu.game.world.creature.characteristics.MutableCharacteristics;
import fr.quatrevieux.araknemu.network.game.GameSession;
import fr.quatrevieux.araknemu.util.ExecutorFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ini4j.Ini;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mockito;

import java.io.File;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.regex.Pattern;

public class GameBaseCase extends DatabaseTestCase {
    static public class SendingRequestStack {
        final public DummyChannel channel;

        public SendingRequestStack(DummyChannel channel) {
            this.channel = channel;
        }

        public void assertLast(Object packet) {
            assertLast(packet.toString());
        }

        public void assertLast(String packet) {
            Assertions.assertEquals(packet, channel.getMessages().peek().toString());
        }

        public void assertLastMatches(String regex) {
            final boolean matches = Pattern.matches(regex, channel.getMessages().peek().toString());

            if (matches) {
                return;
            }

            Assertions.fail("Packet '" + channel.getMessages().peek() + "' does not match regex '" + regex + "'");
        }

        public void assertCount(int count) {
            Assertions.assertEquals(count, channel.getMessages().size());
        }

        public void assertAll(Object... packets) {
            Assertions.assertArrayEquals(
                Arrays.stream(packets).map(Object::toString).toArray(),
                channel.getMessages().stream().map(Object::toString).toArray()
            );
        }

        public void assertEmpty() {
            Assertions.assertTrue(channel.getMessages().isEmpty());
        }

        public void clear() {
            channel.getMessages().clear();
        }

        public void assertContains(Class type) {
            for (Object message : channel.getMessages()) {
                if (type.isInstance(message)) {
                    return;
                }
            }

            Assertions.fail("Cannot find packet of type" + type.getSimpleName());
        }

        public void assertNotContains(Class type) {
            for (Object message : channel.getMessages()) {
                if (type.isInstance(message)) {
                    Assertions.fail("A packet of type " + type.getSimpleName() + " has been found");
                }
            }
        }

        public void assertNotContainsPrefix(String prefix) {
            for (Object message : channel.getMessages()) {
                if (message.toString().startsWith(prefix)) {
                    Assertions.fail("Packet '" + message + "' is not expected");
                }
            }
        }

        public void assertOne(Object packet) {
            for (Object message : channel.getMessages()) {
                if (message.toString().equals(packet.toString())) {
                    return;
                }
            }

            Assertions.fail("Cannot find packet " + packet + "\nAvailable packets : " + channel.getMessages());
        }

        public void dump() {
            for (Object message : channel.getMessages()) {
                System.out.println("[" + message.getClass().getSimpleName() + "] " + message);
            }
        }
    }

    static public class ConnectorModule implements ContainerModule {
        @Override
        public void configure(ContainerConfigurator configurator) {
            configurator.persist(
                RealmConnector.class,
                c -> Mockito.mock(RealmConnector.class)
            );
        }
    }

    protected Container container;
    protected GameConfiguration configuration;
    private Ini initConfig;
    protected DummyServer<GameSession> server;
    protected DummyChannel channel;
    protected GameSession session;
    protected SendingRequestStack requestStack;
    protected Araknemu app;
    protected GameDataSet dataSet;
    protected Accessors accessors;

    @BeforeEach
    public void setUp() throws Exception {
        super.setUp();

        RandomUtil.enableTestingMode();
        ExecutorFactory.enableTestingMode();

        Configuration conf = new DefaultConfiguration(
            new IniDriver(initConfig = new Ini(new File("src/test/test_config.ini")))
        );

        app = new Araknemu(
            conf,
            new DefaultDatabaseHandler(
                conf.module(DatabaseConfiguration.MODULE),
                LogManager.getLogger()
            )
        );

        container = new ItemPoolContainer();
        container.register(new ConnectorModule());
        container.register(new GameModule(app));
        container.register(new AdminModule(app));
        container.register(new SqlLivingRepositoriesModule(
            app.database().get("game")
        ));
        container.register(new SqlWorldRepositoriesModule(
            app.database().get("game")
        ));
        server = new DummyServer<>(container.get(SessionFactory.class));
        container.register(configurator -> configurator.set(Server.class, server));

        configuration = container.get(GameConfiguration.class);

        dataSet = new GameDataSet(
            container,
            new ConnectionPoolExecutor(app.database().get("game"))
        );

        dataSet
            .declare(Account.class, AccountRepository.class)
            .declare(Player.class, PlayerRepository.class)
            .declare(PlayerRace.class, PlayerRaceRepository.class)
            .declare(MapTemplate.class, MapTemplateRepository.class)
            .declare(MapTrigger.class, MapTriggerRepository.class)
            .declare(SubArea.class, SubAreaRepository.class)
            .declare(ItemTemplate.class, ItemTemplateRepository.class)
            .declare(PlayerItem.class, PlayerItemRepository.class)
            .declare(ItemSet.class, ItemSetRepository.class)
            .declare(SpellTemplate.class, SpellTemplateRepository.class)
            .declare(PlayerSpell.class, PlayerSpellRepository.class)
            .declare(PlayerExperience.class, PlayerExperienceRepository.class)
            .declare(ItemType.class, ItemTypeRepository.class)
            .declare(NpcTemplate.class, NpcTemplateRepository.class)
            .declare(Npc.class, NpcRepository.class)
            .declare(Question.class, QuestionRepository.class)
            .declare(ResponseAction.class, ResponseActionRepository.class)
            .declare(MonsterTemplate.class, MonsterTemplateRepository.class)
            .declare(MonsterGroupData.class, MonsterGroupDataRepository.class)
            .declare(MonsterGroupPosition.class, MonsterGroupPositionRepository.class)
            .declare(MonsterRewardData.class, MonsterRewardRepository.class)
            .declare(MonsterRewardItem.class, MonsterRewardItemRepository.class)
            .declare(AccountBank.class, AccountBankRepository.class)
            .declare(BankItem.class, BankItemRepository.class)
            .declare(NpcExchange.class, NpcExchangeRepository.class)
            .declare(Area.class, AreaRepository.class)
            .declare(ConnectionLog.class, ConnectionLogRepository.class)
            .declare(Banishment.class, BanishmentRepository.class)
            .declare(BanIp.class, BanIpRepository.class)
            .use(BanIp.class)
        ;

        session = server.createSession();
        channel = (DummyChannel) session.channel();
        requestStack = new SendingRequestStack(channel);
        accessors = new Accessors();

        container.get(GameService.class).subscribe();
    }

    @AfterEach
    public void tearDown() throws ContainerException {
        ExecutorFactory.resetTestingExecutor();
        dataSet.destroy();
        app.database().stop();
    }

    public void assertClosed() {
        Assertions.assertFalse(channel.isAlive());
    }

    public void login() throws ContainerException {
        GameAccount account = new GameAccount(
            new Account(1, "toto", "", "bob", EnumSet.noneOf(Permission.class), "my question", "my response"),
            container.get(AccountService.class),
            configuration.id()
        );

        account.attach(session);
        dataSet.push(new ConnectionLog(account.id(), Instant.now(), "127.0.0.1"));
        session.setLog(container.get(SessionLogService.class).load(session));
    }

    public GamePlayer gamePlayer() throws ContainerException, SQLException {
        return gamePlayer(false);
    }

    public GamePlayer gamePlayer(boolean load) throws ContainerException, SQLException {
        dataSet
            .use(PlayerItem.class)
            .use(PlayerSpell.class)
        ;

        if (!session.isLogged()) {
            login();
        }

        if (session.player() != null) {
            return session.player();
        }

        dataSet
            .pushSpells()
            .pushRaces()
            .pushExperience()
        ;

        container.get(PlayerExperienceService.class).init(container.get(Logger.class));

        MutableCharacteristics characteristics = new DefaultCharacteristics();

        characteristics.set(Characteristic.STRENGTH, 50);
        characteristics.set(Characteristic.INTELLIGENCE, 150);

        Player player = dataSet.push(new Player(-1, session.account().id(), session.account().serverId(), "Bob", Race.FECA, Gender.MALE, new Colors(123, 456, 789), 50, characteristics, new Position(10540, 200), EnumSet.allOf(ChannelType.class), 0, 0, Integer.MAX_VALUE, 5481459, new Position(10540, 200), 15225));

        if (!load) {
            session.setPlayer(
                new GamePlayer(
                    session.account(),
                    player,
                    container.get(PlayerRaceService.class).get(Race.FECA),
                    session,
                    container.get(PlayerService.class),
                    container.get(InventoryService.class).load(player),
                    container.get(SpellBookService.class).load(session, player),
                    container.get(PlayerExperienceService.class).load(session, player)
                )
            );
        } else {
            session.setPlayer(
                container.get(PlayerService.class).load(
                    session,
                    session.account(),
                    player.id()
                )
            );
        }

        return session.player();
    }

    public ExplorationPlayer explorationPlayer() throws ContainerException, SQLException {
        if (!session.isLogged()) {
            login();
        }

        if (session.exploration() != null) {
            return session.exploration();
        }

        GamePlayer player = gamePlayer(true);
        player.setPosition(new Position(10300, 279));

        dataSet
            .pushAreas()
            .pushSubAreas()
            .pushMaps()
        ;

        ExplorationPlayer explorationPlayer = container.get(ExplorationService.class).create(player);

        session.setExploration(explorationPlayer);
        explorationPlayer.dispatch(new StartExploration(explorationPlayer));

        return explorationPlayer;
    }

    public GamePlayer makeOtherPlayer() throws Exception {
        return makeOtherPlayer(1);
    }

    public GameSession makeOtherPlayerSession() throws Exception {
        return makeOtherPlayerSession(1);
    }

    public GamePlayer makeOtherPlayer(int level) throws Exception {
        return makeOtherPlayerSession(level).player();
    }

    public GameSession makeOtherPlayerSession(int level) throws Exception {
        dataSet
            .pushSpells()
            .pushRaces()
            .pushExperience()
            .use(PlayerItem.class)
            .use(PlayerSpell.class)
        ;

        container.get(PlayerExperienceService.class).init(container.get(Logger.class));

        Player player = dataSet.push(new Player(-1, 5, 2, "Other", Race.CRA, Gender.MALE, new Colors(-1, -1, -1), level, new DefaultCharacteristics(), new Position(10540, 210), EnumSet.allOf(ChannelType.class), 0, 0, Integer.MAX_VALUE, 0, new Position(10540, 210), 0));
        GameSession session = server.createSession();

        // @todo à tester
        session.attach(new GameAccount(
            new Account(5, null, null, null, EnumSet.noneOf(Permission.class), null, null),
            container.get(AccountService.class),
            2
        ));

        session.account().attach(session);

        GamePlayer gp =  container.get(PlayerService.class).load(
            session,
            session.account(),
            player.id()
        );

        session.setPlayer(gp);

        return session;
    }

    public ExplorationPlayer makeOtherExplorationPlayer() throws Exception {
        GamePlayer player = makeOtherPlayer();
        ExplorationPlayer explorationPlayer = container.get(ExplorationService.class).create(player);

        Field sessionField = GamePlayer.class.getDeclaredField("session");
        sessionField.setAccessible(true);

        GameSession.class.cast(sessionField.get(player)).setExploration(explorationPlayer);

        return explorationPlayer;
    }

    public ExplorationPlayer makeExplorationPlayer(GamePlayer player) throws Exception {
        ExplorationPlayer explorationPlayer = container.get(ExplorationService.class).create(player);

        Field sessionField = GamePlayer.class.getDeclaredField("session");
        sessionField.setAccessible(true);

        GameSession.class.cast(sessionField.get(player)).setExploration(explorationPlayer);

        return explorationPlayer;
    }

    public GamePlayer makeSimpleGamePlayer(int id) throws ContainerException, SQLException {
        GameSession session = server.createSession();
        return makeSimpleGamePlayer(id, session);
    }

    public GameSession makeSimpleExplorationSession(int id) throws ContainerException, SQLException {
        GameSession session = server.createSession();
        GamePlayer gp = makeSimpleGamePlayer(id, session);

        ExplorationPlayer ep = new ExplorationPlayer(gp);
        session.setExploration(ep);

        return session;
    }

    public GamePlayer makeSimpleGamePlayer(int id, GameSession session) throws ContainerException, SQLException {
        return makeSimpleGamePlayer(id, session, false);
    }

    public GamePlayer makeSimpleGamePlayer(int id, GameSession session, boolean load) throws ContainerException, SQLException {
        dataSet
            .pushSpells()
            .pushRaces()
            .pushExperience()
            .use(PlayerItem.class)
            .use(PlayerSpell.class)
        ;

        container.get(PlayerExperienceService.class).init(container.get(Logger.class));

        Player player = dataSet.pushPlayer(dataSet.createPlayer(id));

        Account account = new Account(player.accountId(), "ACCOUNT_" + id, "test", "ACCOUNT_" + id, EnumSet.noneOf(Permission.class), "", "");
        GameAccount ga = new GameAccount(account, container.get(AccountService.class), 2);
        ga.attach(session);

        GamePlayer gp;

        if (load) {
            gp = container.get(PlayerService.class).load(session, session.account(), id);
        } else {
            gp = new GamePlayer(
                new GameAccount(
                    new Account(player.accountId()),
                    container.get(AccountService.class),
                    2
                ),
                player,
                container.get(PlayerRaceService.class).get(Race.FECA),
                session,
                container.get(PlayerService.class),
                container.get(InventoryService.class).load(player),
                container.get(SpellBookService.class).load(session, player),
                container.get(PlayerExperienceService.class).load(session, player)
            );
        }

        session.setPlayer(gp);

        return gp;
    }

    public void handlePacket(Packet packet) throws Exception {
        container.get(Dispatcher.class).dispatch(session, packet);
    }

    public void setConfigValue(String item, String value) {
        setConfigValue("game", item, value);
    }

    public void setConfigValue(String config, String item, String value) {
        initConfig.get(config).add(item, value);
    }

    public void removeConfigValue(String config, String item) {
        initConfig.get(config).remove(item);
    }
}
