/*
Copyright (C) 2005 David Green <green@couchpotato.net>
All Rights Reserved.

This file is part of Aelfengard.

Aelfengard is proprietary software. You may not redistribute it without
prior written permission from the copyright holder.
*/

package server;

import java.awt.Color;
import java.awt.Point;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.crypto.Cipher;

import server.command.CommandProcessor;
import server.command.MailCommand;
import server.command.RoomCommand;
import server.scripting.Script;
import server.scripting.ScriptingLanguage;
import server.scripting.ScriptingSystem;
import server.token.TokenString;

import common.ByteSource;
import common.Face;
import common.PubGroupMember;
import common.PubRoom;
import common.PubRoomEntity;
import common.ScriptType;
import common.TextEditorType;
import common.clientevent.ClientEvent;
import common.clientevent.ClientEventType;
import common.clientevent.CommandClientEvent;
import common.clientevent.EditRoomClientEvent;
import common.clientevent.ExecuteManualScriptClientEvent;
import common.clientevent.FaceChooserClientEvent;
import common.clientevent.ImageUploadClientEvent;
import common.clientevent.MailCreateClientEvent;
import common.clientevent.SaveScriptClientEvent;
import common.gameevent.ClearScreenGameEvent;
import common.gameevent.CommandLineColorGameEvent;
import common.gameevent.ExecuteManualScriptResponseGameEvent;
import common.gameevent.FaceChooserGameEvent;
import common.gameevent.FaceGameEvent;
import common.gameevent.GroupGameEvent;
import common.gameevent.ImageGameEvent;
import common.gameevent.ImageUploadGameEvent;
import common.gameevent.LocationGameEvent;
import common.gameevent.RoomDestroyedGameEvent;
import common.gameevent.RoomEditorGameEvent;
import common.gameevent.RoomEntityGameEvent;
import common.gameevent.RoomUpdatedGameEvent;
import common.gameevent.SaveScriptResponseGameEvent;
import common.gameevent.ScriptEditorGameEvent;
import common.gameevent.StatGameEvent;
import common.gameevent.TableGameEvent;
import common.gameevent.TextEditorGameEvent;
import common.gameevent.TextGameEvent;
import common.gameevent.TrackGameEvent;


public class ClientState implements RoomListener, TrackingListener, GroupListener, AreaListener {

    private final Player player;
    private final Client client;

    private Area area;
    private Room room;
    private Group group;

    private Set<Player> trackSet;
    
    private Set<RoomEntity> roomEntitySet = new HashSet<RoomEntity>();
    private Set<Player> groupMemberSet = new HashSet<Player>();

    private boolean closed;
    
    private ClientState(SelectionKey key, Player player, Cipher encipher, Cipher decipher) {
        this.player = player;
        this.client = new Client(key, this, encipher, decipher);
        key.attach(client);

        player.goOnline(this);

        setGroup(player.getGroup());
        setRoom(player.getRoom());

        send(area.makeMapGameEvent());

        sendLocationGameEvent();
        
        recalcCommandLineColor();

        recalcTrackingMode();
        
        String loginCommand = System.getProperty("aelfengard.loginCommand"); 
        if (loginCommand != null && player.getId() != 1) {
            try {
                Runtime.getRuntime().exec(loginCommand);
            }
            catch (IOException ex) {
                /* ignore */
            }
        }
    }
    
    public void close(boolean replaced) {
        if (closed) {
            return;
        }
        closed = true;
        client.close();
        if (trackSet != null) {
            TrackingSystem.removeTrackingListener(this);
        }
        if (room != null) {
            room.removeRoomListener(this);
        }
        if (area != null) {
            area.removeAreaListener(this);
        }
        if (group != null) {
            group.removeGroupListener(this);
        }
        if (!replaced) {
            player.goOffline();
        }
    }

    public static void init(SelectionKey key, Player player, Cipher encipher, Cipher decipher) {
        new ClientState(key, player, encipher, decipher);
    }
    
    public void sendText(boolean skipLine, String text) {
        send(new TextGameEvent(skipLine, text));
    }
    
    public void sendImage(String image, String description) {
        send(new ImageGameEvent(image, description));
    }
    
    public void sendFace(Face face, String description) {
        send(new FaceGameEvent(face, description));
    }
    
    public void setGroup(Group group) {
        if (group == this.group) {
            return; // nothing to do
        }
        if (this.group != null) {
            this.group.removeGroupListener(this);
        }
        this.group = group;
        group.addGroupListener(this);
        refreshGroupMembers(true);
    }
    
    private void setArea(Area area) {
        if (this.area == area) {
            return; // nothing to do
        }
        if (this.area != null) {
            this.area.removeAreaListener(this);
        }
        this.area = area;
        send(area.makeMapGameEvent());
        area.addAreaListener(this);
    }
    
    public void setRoom(Room room) {
        if (this.room == room) {
            return; // nothing to do
        }
        if (this.room != null) {
            this.room.removeRoomListener(this);
        }
        setArea(room.getArea());
        this.room = room;
        room.addRoomListener(this);
        sendLocationGameEvent();
        refreshRoomEntities();
        refreshGroupMembers(false);
    }
    
    public void recalcTrackingMode() {
        boolean tracking = player.getAdminFlags().contains(AdminFlag.TRACKING);
        if (tracking == (trackSet != null)) {
            return; // nothing to do
        }
        if (tracking) {
            trackSet = new HashSet<Player>();
            TrackingSystem.addTrackingListener(this);
            // send all existing players
            for (Player p : Player.getOnlinePlayers()) {
                trackingChanged(p, p.getRoom());
            }
        }
        else {
            TrackingSystem.removeTrackingListener(this);
            for (Player p : trackSet) {
                send(new TrackGameEvent(p.getId(), null));
            }
            trackSet = null;
        }
    }

    public void roomImageChanged(String filename) {
        sendLocationGameEvent();
    }
    
    private void refreshGroupMembers(boolean clear) {
        List<PubGroupMember> list = new ArrayList<PubGroupMember>();
        groupMemberSet = new HashSet<Player>();
        // add myself to top of list
        groupMemberSet.add(player);
        list.add(player.getPubGroupMember(player));
        // add other group members next
        for (Player p : group.players()) {
            if (p != player) {
                groupMemberSet.add(p);
                list.add(player.getPubGroupMember(p));
            }
        }
        PubGroupMember[] pgm = new PubGroupMember[list.size()];
        list.toArray(pgm);
        send(new GroupGameEvent(clear ? GroupGameEvent.EventType.CLEAR : GroupGameEvent.EventType.MODIFY, pgm));
    }

    public void refreshRoomEntities() {
        List<PubRoomEntity> list = new ArrayList<PubRoomEntity>();
        roomEntitySet = new HashSet<RoomEntity>();
        for (RoomEntity entity : room.getRoomEntities()) {
            if (entity != player && entity.isVisibleTo(player)) {
                roomEntitySet.add(entity);
                list.add(player.getPubRoomEntity(entity));
            }
        }
        PubRoomEntity[] pre = new PubRoomEntity[list.size()];
        list.toArray(pre);
        send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.CLEAR, pre));
    }

    public void roomEntityAdded(RoomEntity entity) {
        if (entity == player) {
            return; // don't add myself
        }
        if (entity.isVisibleTo(player)) {
            if (roomEntitySet.add(entity)) {
                send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.ADD, player.getPubRoomEntity(entity)));
            }
        }
    }

    public void roomEntityRemoved(RoomEntity entity) {
        if (entity == player) {
            return; // don't remove myself
        }
        if (roomEntitySet.remove(entity)) {
            send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.REMOVE, player.getPubRoomEntity(entity)));
        }
    }
    
    public void roomEntityChanged(RoomEntity entity, TokenString msg) {
        if (entity == player) {
            return; // don't modify myself
        }
        boolean visibleNow = entity.isVisibleTo(player);
        boolean visibleBefore = roomEntitySet.contains(entity); 
        if (visibleNow || visibleBefore) {
            if (msg != null) {
                player.sendText(true, null, null, null, msg, null, entity);
            }
            if (visibleNow && visibleBefore) {
                send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.MODIFY, player.getPubRoomEntity(entity)));
            }
            else if (visibleNow) {
                // appeared
                roomEntitySet.add(entity);
                send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.ADD, player.getPubRoomEntity(entity)));
            }
            else {
                // disappeared
                roomEntitySet.remove(entity);
                send(new RoomEntityGameEvent(RoomEntityGameEvent.EventType.REMOVE, player.getPubRoomEntity(entity)));
            }
        }
    }

    public void trackingChanged(Player target, Room theirRoom) {
        if (target == player) {
            return; // don't track myself
        }
        if (theirRoom != null && theirRoom.getArea() == area) {
            trackSet.add(target);
            send(new TrackGameEvent(target.getId(), new Point(theirRoom.getX(), theirRoom.getY())));
        }
        else if (trackSet.remove(target)) {
            send(new TrackGameEvent(target.getId(), null));
        }
    }
    
    public void process(byte[] b) throws Exception {
        ClientEvent e = ClientEvent.forByteArray(b);
        ClientEventType type = e.getClientEventType();
        switch (type) {
            case COMMAND: {
                CommandClientEvent evt = (CommandClientEvent) e; 
                CommandProcessor.process(player, evt.getCommand());
                return;
            }
            case EDIT_ROOM: {
                EditRoomClientEvent evt = (EditRoomClientEvent) e;
                RoomCommand.notifyRoomEditComplete(player, evt);
                return;
            }
            case FACE_CHOOSER: {
                FaceChooserClientEvent evt = (FaceChooserClientEvent) e;
                player.setFace(evt.getFace());
                sendText(true, "Your face choices have been saved.");
                return;
            }
            case MAIL_CREATE: {
                MailCreateClientEvent evt = (MailCreateClientEvent) e;
                MailCommand.notifyMailCreateComplete(player, evt);
                return;
            }
            case CONSOLE_COMMAND: {
                ExecuteManualScriptClientEvent evt = (ExecuteManualScriptClientEvent) e;
                runScriptingCommand(evt);
                return;
            }
            case CONSOLE_SAVE: {
                SaveScriptClientEvent evt = (SaveScriptClientEvent) e;
                saveScript(evt);
                return;
            }
            case IMAGE_UPLOAD: {
                ImageUploadClientEvent evt = (ImageUploadClientEvent) e;
                uploadImage(evt);
                return;
            }
            default: throw new FatalError("Unrecognized client event type: " + type);
        }
    }
    
    private void uploadImage(ImageUploadClientEvent evt) {
        final NPC npc = IdType.getNPCById(evt.getNPCId());
        if (npc == null) {
            player.sendText(true, "That NPC no longer exists.");
            return;
        }
        final String[] filename = new String[] { evt.getFilename() };
        //player.sendText(true, "@10FWriting: @2ZZ" + newImage);                    
        Utils.uploadData(player, filename, evt.getUploadData(), new Runnable() {
            public void run() {
                if (npc.isDestroyed()) {
                    player.sendText(true, "That NPC no longer exists.");
                    return;
                }
                npc.setImage(filename[0]);
                player.sendText(true, "@10FNPC image has been set to uploaded image.");
            }
        });
    }
    
    private void saveScript(final SaveScriptClientEvent evt) {
        if (!player.getAdminFlags().contains(AdminFlag.CODER)) {
            send(new SaveScriptResponseGameEvent(evt.getConsoleId(), true, "Console access denied."));
            return;
        }
        if (evt.getScriptType() == null) {
            send(new SaveScriptResponseGameEvent(evt.getConsoleId(), true, "Invalid script type."));
        }
        String filename = evt.getFilename().trim().toLowerCase();
        if (filename == null || filename.length() == 0) {
            send(new SaveScriptResponseGameEvent(evt.getConsoleId(), true, "Please enter a filename."));
            return;
        }
        for (int i = 0; i < filename.length(); i++) {
            if (ScriptingSystem.VALID_FILENAME_CHARS.indexOf(filename.charAt(i)) < 0) {
                send(new SaveScriptResponseGameEvent(evt.getConsoleId(), true, "Invalid filename. Only [A-Za-z0-9._] allowed."));
                return;
            }
        }
        ScriptingSystem.saveScript(evt.getScriptType(), filename, ScriptingLanguage.GROOVY, evt.getScript());
        send(new SaveScriptResponseGameEvent(evt.getConsoleId(), false, "Script saved."));
    }
    
    private void runScriptingCommand(final ExecuteManualScriptClientEvent evt) {
        if (!player.getAdminFlags().contains(AdminFlag.CODER)) {
            sendText(true, "Console access denied.");
            return;
        }
        Script script = new Script(evt.getScriptType(), "", ScriptingLanguage.GROOVY, evt.getCommand());
        String response = script.run(new HashMap<String,Object>());
        send(new ExecuteManualScriptResponseGameEvent(evt.getConsoleId(), response));
    }

    private void sendLocationGameEvent() {
        send(new LocationGameEvent(new Point(room.getX(), room.getY()), room.getImage()));
    }

    public void groupMemberAdded(Player p) {
        if (groupMemberSet.add(p)) {
            send(new GroupGameEvent(GroupGameEvent.EventType.ADD, player.getPubGroupMember(p)));
        }
    }

    public void groupMemberChanged(Player p) {
        if (groupMemberSet.contains(p)) {
            send(new GroupGameEvent(GroupGameEvent.EventType.MODIFY, player.getPubGroupMember(p)));
        }
    }

    public void groupMemberRemoved(Player p) {
        if (groupMemberSet.remove(p)) {
            send(new GroupGameEvent(GroupGameEvent.EventType.REMOVE, player.getPubGroupMember(p)));
        }
    }

    public void roomChanged(PubRoom newRoom) {
        send(new RoomUpdatedGameEvent(newRoom));
    }

    public void roomDeleted(Point p) {
        send(new RoomDestroyedGameEvent(p));
    }

    public void launchRoomEditor(Room roomToEdit, boolean isAdmin) {
        send(new RoomEditorGameEvent(
                roomToEdit.makePubRoom(),
                roomToEdit.getDescription(null),
                player.getUploadDirectory(),
                roomToEdit.getImage(),
                isAdmin));
    }
    
    public void launchTextEditor(TextEditorType type, int id, String text) {
        send(new TextEditorGameEvent(type, id, text));
    }
    
    public void launchFaceChooser(Face face) {
        send(new FaceChooserGameEvent(face));
    }
    
    public void launchConsole(ScriptType type, String filename, String text) {
        send(new ScriptEditorGameEvent(type, filename, text));
    }

    public void refreshEntity(RoomEntity entity) {
        if (roomEntitySet.contains(entity)) {
            roomEntityChanged(entity, null);
        }
        if (!(entity instanceof Player)) {
            return;
        }
        Player p = (Player) entity;
        if (groupMemberSet.contains(p)) {
            groupMemberChanged(p);
        }
    }

    public void recalcCommandLineColor() {
        Color fg, bg;
        boolean afk = player.isAFK();
        boolean invis = player.isAdminInvisible();
        if (afk) {
            fg = Color.white;
            bg = Color.red;
        }
        else if (invis) {
            fg = Color.black;
            bg = Color.yellow;
        }
        else {
            fg = null;
            bg = null;
        }
        send(new CommandLineColorGameEvent(fg, bg));
    }

    public void sendTable(String[][] data, String[] headers, int[] sizes) {
        send(new TableGameEvent(data, headers, sizes));
    }

    public void roomEntityHealth(RoomEntity entity, long hp, long maxHP) {
        if (roomEntitySet.contains(entity)) {
            send(new StatGameEvent(StatGameEvent.TableType.ROOM_ENTITY_TABLE, entity.getId(), StatGameEvent.StatType.HEALTH, hp, maxHP));
        }
    }

    public void groupMemberHealth(Player target, long hp, long maxHP) {
        if (groupMemberSet.contains(target)) {
            send(new StatGameEvent(StatGameEvent.TableType.GROUP_MEMBER_TABLE, target.getId(), StatGameEvent.StatType.HEALTH, hp, maxHP));
        }
    }
    
    void send(ByteSource bs) {
        client.send(bs);
    }

    public void clearScreen() {
        send(new ClearScreenGameEvent());
    }

    public void doImageUpload(int npcId) {
        send(new ImageUploadGameEvent(npcId));
    }

}