package com.alterdekim.game.component;

import com.alterdekim.game.component.game.*;
import com.alterdekim.game.component.game.friends.OneFR;
import com.alterdekim.game.component.game.friends.UserFriend;
import com.alterdekim.game.component.game.inventory.BuyGoodResult;
import com.alterdekim.game.component.game.inventory.BuyGoodResultGood;
import com.alterdekim.game.component.game.response.init.GetUserInfo;
import com.alterdekim.game.component.game.response.location.AddUserToLocation;
import com.alterdekim.game.component.rtmp.ConnectedProcessor;
import com.alterdekim.game.entity.Good;
import com.alterdekim.game.entity.PhoneMessage;
import com.alterdekim.game.entity.Postcard;
import com.alterdekim.game.message.*;
import com.alterdekim.game.message.amf.AMFMapper;
import com.alterdekim.game.message.amf.AMFObject;
import com.alterdekim.game.message.amf.AMFSerializer;
import com.alterdekim.game.message.amf.AMFValueType;
import com.alterdekim.game.security.AuthenticationUtil;
import com.alterdekim.game.service.*;
import com.alterdekim.game.utils.GameUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.javatuples.Pair;
import org.javatuples.Triplet;
import org.javatuples.Tuple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Component
public class GameServer {

    @Autowired
    private UserService users;

    @Autowired
    private LocationService locationService;

    @Autowired
    private RelationshipService relationshipService;

    @Autowired
    private PhoneMessageService phoneMessageService;

    @Autowired
    private PostcardService postcardService;

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private MiniGameService miniGameService;

    private final Map<Integer, Player> players;

    private final ObjectMapper xmlMapper;

    public GameServer() {
        this.players = new HashMap<>();
        this.xmlMapper = new XmlMapper().registerModule(new JavaTimeModule());
    }

    public void onConnect(int playerId, String hwId, ConnectedProcessor connectedProcessor) {
        log.warn("GameServer.onConnect() connect: {}", playerId);
        Optional<String> username = users.getUsernameById(playerId);
        if( username.isEmpty() || !AuthenticationUtil.hwIdAuth(users.findByUsername(username.get()), hwId) || this.players.containsKey(playerId) ) {
            connectedProcessor.close();
            return;
        }
        this.players.put(playerId, new Player(connectedProcessor, username.get()));
    }

    public void onMessage(int playerId, List<AMFObject> params ) {
        try {
            log.info("GameServer.onMessage() pid: {}, params: {}", playerId, params);
            switch(CommandType.fromString( params.get(0).toString() )) {
                case UserCommand:
                    processUserMessage(playerId, AMFMapper.deobfuscate(params.subList(1, params.size()), UserMessage.class));
                    break;
                case SetLocation:
                    setPlayerLocation(playerId, AMFMapper.deobfuscate(params.subList(1, params.size()), SetLocationMessage.class));
                    break;
                case SetUserAvatarState:
                    setPlayerAvatarState(playerId, AMFMapper.deobfuscate(params.subList(1, params.size()), SetPlayerAvatarState.class));
                    break;
                case SetUserAvatarPosition:
                    setPlayerAvatarPosition(playerId, AMFMapper.deobfuscate(params.subList(1, params.size()), SetPlayerAvatarPosition.class));
                    break;
                default:
                    log.warn("GameServer.onMessage() unknown command: {}", params.get(0));
            }
        } catch (Exception e) {
            log.error("GameServer.onMessage() error: {}", e.getMessage(), e);
        }
    }

    private void setPlayerAvatarPosition(int playerId, SetPlayerAvatarPosition message) {
        if( message.getCoordinates() != null ) {
            players.get(playerId).setX(message.getCoordinates().get("x").getDouble());
            players.get(playerId).setY(message.getCoordinates().containsKey("y") ? message.getCoordinates().get("y").getDouble() : -100);
        }
        this.sendInPlayersLocation(playerId, CommandType.SetUserAvatarPosition,
                new SetPlayerPosition(
                        playerId,
                        new Point(players.get(playerId).getX(), players.get(playerId).getY()),
                        message.getTweenerId().longValue()
                )
        );
    }

    private void setPlayerAvatarState(int playerId, SetPlayerAvatarState message) {
        players.get(playerId).setState(message.getState());
        this.sendInPlayersLocation(playerId, CommandType.SetUserAvatarState,
                new SetPlayerState(playerId, message.getState())
        );
    }

    private void processUserMessage(int playerId, UserMessage message) throws JsonProcessingException {
        log.info("GameServer.processUserMessage() message: {}", message);
        switch (UserCommandType.fromString(message.getCommandType())) {
            case GetUserInitData -> {
                String r = xmlMapper.writeValueAsString(users.getUserInitInfoByUserId(playerId));
                this.sendResult(playerId, message.getTransactionId(), r);
            }
            case UserFriendsGet, GetOnlineUserFriends -> this.sendResult(playerId, message.getTransactionId(), users.getFriendsOfUser(playerId, players));
            case UserFriendsRequests -> this.sendResult(playerId, message.getTransactionId(), users.getRequestsOfUser(playerId, players));
            case RevokeUserFriendship -> {
                users.removeFriendshipWithUser(playerId, message.getArguments().get(0).getInt());
                this.call(message.getArguments().get(0).getInt(), CommandType.OnFriendsRemoved, playerId);
            }
            case ApplyUserFriendship -> this.applyUserFriendship(playerId, message.getArguments().get(0).toString(), message.getArguments().get(1).toString());
            case GetClubMap -> // todo: implement
                    this.sendResult(playerId, message.getTransactionId(), "</clubmap>");
            case GetFriendPanelData -> this.sendResult(playerId, message.getTransactionId(), "");
            case UpdateUserData -> this.sendResult(playerId, message.getTransactionId(), null);
            case GetUserLocation -> {
                double prevLocation = message.getArguments().get(2).getDouble();
                if (prevLocation == 0.0d) {
                    this.sendResult(playerId,
                            message.getTransactionId(),
                            xmlMapper.writeValueAsString(
                                    locationService.getDefaultLocation()
                            ).replace("&lt;", "<")
                    );
                    return;
                }
                long requestedLocation = message.getArguments().get(1).getLong();
                if( requestedLocation == -1L ) {
                    int homePlayerId = message.getArguments().get(0).getInt();
                    this.sendResult(playerId, message.getTransactionId(), xmlMapper.writeValueAsString(locationService.getHomeByUserId(homePlayerId)).replace("&lt;", "<"));
                    return;
                }
                if( requestedLocation == -2L ) {

                    return;
                }
                this.sendResult(playerId, message.getTransactionId(), xmlMapper.writeValueAsString(locationService.getLocationById(requestedLocation)).replace("&lt;", "<"));
            }
            case GetUserLocationById -> {
                long nextLocation = message.getArguments().get(1).getLong();
                if (nextLocation == -1) {
                    nextLocation = 0;
                }
                this.sendResult(playerId, message.getTransactionId(), xmlMapper.writeValueAsString(locationService.getLocationById(nextLocation)).replace("&lt;", "<"));
            }
            case GetCatalogInfo -> {
                long catalogObjectId = message.getArguments().get(0).getLong();
                // todo: implement
            }
            case ChatMessage -> {
                String text = message.getArguments().get(1).toString();
                sendInPlayersLocation(playerId, CommandType.ChatMessage, new ChatMessage(playerId, text));
            }
            case Shoot -> {
                if (message.getSrv().equals("ROOM")) {
                    double x = message.getArguments().get(1).getDouble();
                    double y = message.getArguments().get(2).getDouble();
                    sendInPlayersLocation(playerId, CommandType.Shoot, new Shoot(playerId, x, y));
                    break;
                }
                this.sendInPlayersLocation(playerId, CommandType.SetUserAvatarState,
                        new SetPlayerState(playerId, players.get(playerId).getState())
                );
                this.users.setWeaponsCount(playerId, this.users.getIntegerUserProperty(playerId, PlayerProperties.WeaponsCount, 15) - 1);
            }
            case GetUserInfo -> getUserInfo(playerId, message.getArguments().get(0).getInt(), message);
            case GetUserAvatar -> getAvatar(playerId, message);
            case SaveUserAvatarChanges -> saveAvatar(playerId, message);
            case GetUserUnlocks -> this.sendResult(playerId, message.getTransactionId(), "123");
            case GetSnInvitePanelData -> {}
            case GetGrantBlocks -> {}
            case UseMagicAbility -> this.sendInPlayersLocation(playerId, CommandType.UseMagicAbility, new UseMagicAbility(
                    message.getArguments().get(0).getInt(),
                    message.getArguments().get(1).getInt()
            ));
            case SetDefaultUserPhone -> {
                long phoneId = message.getArguments().get(0).getLong();
                this.users.setDefaultPhone(playerId, phoneId);
            }
            case SetUserBackground -> {
                long bgId = message.getArguments().get(0).getLong();
                this.users.setDefaultBackground(playerId, bgId);
            }
            case QueryUserFriendship -> {
                int participantId = message.getArguments().get(0).getInt();
                relationshipService.sendRequest(playerId, participantId);
                this.call(participantId, CommandType.OnFriendshipRequestAdded,
                        new OneFR(
                                new UserFriend(playerId, players.get(playerId).getUsername(), players.get(playerId).getUsername(), true)
                        )
                );
            }
            case GetPhoneBalance -> {
                Integer balance = users.getUserAccountByUserId(playerId).getPhoneCardBalance();
                this.sendResult(playerId, message.getTransactionId(), balance);
            }
            case PhoneMessage -> {
                int receiverId =  message.getArguments().get(0).getInt();
                if (receiverId <= 0) return;
                String messageText = message.getArguments().get(1).toString();
                PhoneMessage pm = new PhoneMessage();
                pm.setText(messageText);
                pm.setSenderId(playerId);
                pm.setReceiverId(receiverId);
                pm.setIsNew(true);
                pm.setSenderName(players.get(playerId).getUsername());
                pm.setDateSent("");
                phoneMessageService.savePhoneMessage(pm);
                this.call(receiverId, CommandType.AddNewPhoneMessage, xmlMapper.writeValueAsString(pm));
            }
            case MarkPhoneMessageAsRead -> {
                Long messageId = message.getArguments().get(0).getLong();
                phoneMessageService.markAsRead(messageId);
            }
            case DeletePhoneMessage -> {
                long messageId1 = message.getArguments().get(0).getLong();
                phoneMessageService.removeById(messageId1);
            }
            case CardMessage -> {
                if( !subtractUsualTickets(playerId, 20) ) return;
                int receiverId = message.getArguments().get(0).getInt();
                int postcardId = message.getArguments().get(1).getInt();
                Postcard postcard = new Postcard();
                postcard.setIsNew(true);
                postcard.setDateSent("");
                postcard.setSenderId(playerId);
                postcard.setReceiverId(receiverId);
                postcard.setSenderName(players.get(playerId).getUsername());
                postcard.setCardId(postcardId);
                postcardService.savePostcard(postcard);
                this.call(receiverId, CommandType.CardMessage, xmlMapper.writeValueAsString(postcard));
            }
            case MarkCardMessageAsRead -> {
                long postcardMessageId = message.getArguments().get(0).getLong();
                this.postcardService.markAsRead(postcardMessageId);
            }
            case DeleteCardMessage -> {
                long postcardMessageId = message.getArguments().get(0).getLong();
                this.postcardService.removeById(postcardMessageId);
            }
            case BuyBatch -> buySomething(playerId, message.getArguments().get(0), message);
            case LockHouse -> lockSomething(playerId, message.getArguments().get(0).getInt(), message.getArguments().get(1).getBoolean(), message);
            case GetGame -> getGame(playerId, message.getArguments().get(0).getLong(), message.getTransactionId());
            case GameScore -> processGameScore(playerId, message.getTransactionId(), message.getArguments().get(1).getInt());
            case SendGameClose, GameStatistics -> {
                this.sendResult(playerId, message.getTransactionId(), "0");
            }
        }
    }

    private void processGameScore(int playerId, double transactionId, int amount) {
        this.addUsualTickets(playerId, amount);
        this.sendResult(playerId, transactionId, "");
    }

    private void getGame(int playerId, long gameId, double transactionId) throws JsonProcessingException {
        this.sendResult(playerId, transactionId, xmlMapper.writeValueAsString(miniGameService.findGameById(gameId).get()));
    }

    private void lockSomething(int playerId, int what2Lock, Boolean state, UserMessage message) {
        if( what2Lock == -2 ) {
            this.users.pushUserProperty(playerId, PlayerProperties.IsClubLocked, state);
        } else {
            this.users.pushUserProperty(playerId, PlayerProperties.IsHomeLocked, state);
        }
        this.sendResult(playerId, message.getTransactionId(), state);
    }

    private void buySomething(int playerId, AMFObject obj, UserMessage message) {
        List<AMFObject> array = (List<AMFObject>) obj.getRaw();
        array.forEach(i -> this.buyOne(playerId, i, message));
    }

    private void buyOne(int playerId, AMFObject item, UserMessage message) {
        Map<String, AMFObject> obj = (Map<String, AMFObject>) item.getRaw();
        obj = (Map<String, AMFObject>) obj.get("item").getRaw();
        long goodId = obj.get("id").getLong();
        Optional<Good> tGood = this.goodsService.findById(goodId);
        if( tGood.isEmpty() ) {
            this.sendResult(playerId, message.getTransactionId(), new BuyGoodResult(new ArrayList<>()));
            return;
        }
        Good good = tGood.get();
        boolean isMagic = good.getMagicTickets() != 0;
        AvatarInventoryType invType = AvatarInventoryType.fromGoodType(good.getGoodTypeId());
        if( isMagic ) {
            if( !subtractMagicTickets(playerId, good.getMagicTickets()) ) {
                this.sendResult(playerId, message.getTransactionId(), new BuyGoodResult(new ArrayList<>()));
                return;
            }
            this.users.pushToInventory(playerId, invType, goodId);
            this.sendResult(playerId, message.getTransactionId(), new BuyGoodResultGood(new BuyGoodResultGood.BuyGoodBody(false, new ArrayList<>())));
            return;
        }
        if( !subtractUsualTickets(playerId, good.getUsualTickets()) ) {
            this.sendResult(playerId, message.getTransactionId(), new BuyGoodResult(new ArrayList<>()));
            return;
        }
        this.users.pushToInventory(playerId, invType, goodId);
        this.sendResult(playerId, message.getTransactionId(), new BuyGoodResultGood(new BuyGoodResultGood.BuyGoodBody(false, new ArrayList<>())));
    }

    private void addUsualTickets(int userId, int amount) {
        int val = this.users.getUsualTickets(userId);
        val += amount;
        this.users.setUsualTickets(userId, val);
        this.call(userId, CommandType.UpdateTickets, new UpdateTickets(val, this.users.getMagicTickets(userId)));
    }

    private boolean subtractUsualTickets(int userId, int amount) {
        int val = this.users.getUsualTickets(userId);
        if ( val >= amount ) {
            val -= amount;
            this.users.setUsualTickets(userId, val);
            this.call(userId, CommandType.UpdateTickets, new UpdateTickets(val, this.users.getMagicTickets(userId)));
            return true;
        }
        return false;
    }

    private boolean subtractMagicTickets(int userId, int amount) {
        int val = this.users.getMagicTickets(userId);
        if ( val >= amount ) {
            val -= amount;
            this.users.setMagicTickets(userId, val);
            this.call(userId, CommandType.UpdateTickets, new UpdateTickets(this.users.getUsualTickets(userId), val));
            return true;
        }
        return false;
    }

    private void applyUserFriendship(Integer userId, String acceptString, String denyString) {
        List<Integer> accepted = Arrays.stream(acceptString.split(","))
                .filter(s -> !s.isEmpty())
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        List<Integer> denied = Arrays.stream(denyString.split(","))
                .filter(s -> !s.isEmpty())
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        users.acceptFriendshipWithUsers(userId, accepted);
        users.denyFriendshipsWithUsers(userId, denied);
        Map<String, UserFriend> m = new HashMap<>();
        m.put("1", new UserFriend(userId, "", players.get(userId).getUsername(), true));
        accepted.forEach(uid -> this.call(uid, CommandType.OnFriendsAdded, m));
    }

    private void saveAvatar(int playerId, UserMessage message) {
        Map<String, AMFObject> changes = (Map<String, AMFObject>) message.getArguments().get(0).getRaw();

        List<AMFObject> bodyParts = (List<AMFObject>) changes.get("BodyParts").getRaw();
        List<AMFObject> inventory = (List<AMFObject>) changes.get("Inventory").getRaw();

        List<Triplet<Long, Object, AvatarInventoryType>> l = Stream.concat(
                bodyParts.stream().map(f -> (Map<String, AMFObject>) f.getRaw()).map(f -> Triplet.with(f.get("Id").getLong(), f.get("Color").getRaw(), AvatarInventoryType.BodyParts)),
                inventory.stream().map(f -> (Map<String, AMFObject>) f.getRaw()).map(f -> Triplet.with(f.get("Id").getLong(), f.get("Color").getRaw(), AvatarInventoryType.fromGoodId(goodsService, f.get("Id").getLong())))
        ).collect(Collectors.toList());

        users.resetUsedClothes(playerId);

        for(var i: l) {
            var s = String.valueOf(i.getValue1());
            int color = 0;
            if( !s.equals("NaN") ) {
                color = ((Double) i.getValue1()).intValue();
            }
            users.setDefaultInventoryItem(playerId, i.getValue0(), i.getValue2(), color);
        }
        this.sendResult(playerId, message.getTransactionId(), new ArrayList<>());
        this.updateLocationPlayers(playerId, this.players.get(playerId).getLocationId());
    }

    private void getAvatar(int playerId, UserMessage message) throws JsonProcessingException {
        String r = xmlMapper.writeValueAsString(users.getUserAvatarByUserId(playerId));
        r = r.substring(13);
        r = r.substring(0, r.length() - 14);
        this.sendResult(playerId, message.getTransactionId(), r);
    }

    private void getUserInfo(int playerId, int targetId, UserMessage message) throws JsonProcessingException {
        String r = xmlMapper.writeValueAsString(new GetUserInfo(users.getUserInfoByUserId(targetId, false, playerId), users.getOtherUserAvatar(targetId)));
        r = r.substring(11);
        r = r.substring(0, r.length()-12);
        this.sendResult(playerId, message.getTransactionId(), r);
    }

    private void setPlayerLocation(int playerId, SetLocationMessage message) {
        Player p = this.players.get(playerId);
        this.deleteSelf(playerId, p.getLocationId());
        long prevLocation = p.getLocationId();
        p.setLocationId(GameUtils.extractLocationId(message.getLocation()));
        p.setX(message.getCoordinates().get("x").getDouble());
        p.setY(message.getCoordinates().get("y").getDouble());
        p.setState(message.getStartState());
        this.updateLocationPlayers(playerId, prevLocation);
        this.sendResult(playerId, message.getTransactionId(), "");
    }

    private void deleteSelf(int playerId, long locationId) {
        Player p = this.players.get(playerId);
        this.sendInLocation(locationId, CommandType.RemoveUserFromLocation, playerId);
    }

    private void updateLocationPlayers(int playerId, long prevLocation) {
        long locationId = this.players.get(playerId).getLocationId();
        this.sendInLocation(prevLocation, CommandType.RemoveUserFromLocation, playerId);
        for( int i = 0; i < 3; i++ ) {
            try {
                Thread.sleep(300);
                this.sendInPlayersLocation(playerId, CommandType.AddUserToLocation, new AddUserToLocation(
                        playerId,
                        users.getAvatarById(playerId, this.players.get(playerId))
                ));
            } catch (InterruptedException ie) {
                log.error("Sleep exception: {}", ie.getMessage());
            }
        }
        this.players.keySet().forEach(pid -> {
            Player p = this.players.get(pid);
            if( p.getLocationId() != locationId ) return;
            this.call(playerId, CommandType.AddUserToLocation, new AddUserToLocation(
                    pid,
                    users.getAvatarById(pid, this.players.get(pid))
            ));
            this.call(playerId, CommandType.SetUserAvatarState, new SetPlayerState(
                    pid,
                    this.players.get(pid).getState()
            ));
        });
    }

    private void sendInPlayersLocation(int playerId, CommandType type, Object obj) {
        Player player = this.players.get(playerId);
        this.sendInLocation(player.getLocationId(), type, obj);
    }

    private void sendInLocation(long locationId, CommandType type, Object obj) {
        players.keySet().forEach(k -> {
            if( locationId != players.get(k).getLocationId() ) return;
            this.call(k, type, obj);
        });
    }

    private void sendResult(int playerId, double tid, Object obj) {
        this.sendMessage(playerId, new CallMessage("_result", tid, null, obj));
    }

    private void call(int playerId, CommandType function, Object obj) {
        this.sendMessage(playerId, new CallMessage(function.getValue(), 0d, null, obj));
    }

    private void sendMessage(int playerId, Object obj) {
        try {
            this.players.get(playerId)
                    .getConnection()
                    .write_packet(AMFSerializer.serialize(obj));
        } catch (IOException e) {
            log.error("Unable to send message: {}", e.getMessage());
        }
    }

    public void onDisconnect(int playerId) {
        log.warn("GameServer.onDisconnect() close: {}", playerId);
        if( !this.players.containsKey(playerId) ) return;
        this.deleteSelf(playerId, this.players.get(playerId).getLocationId());
        this.players.remove(playerId);
    }

    public int getPlayersCount() {
        return this.players.size();
    }
}