package openperipheral.addons.glasses;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.mojang.authlib.GameProfile;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.server.MinecraftServer;
import net.minecraftforge.common.util.ForgeDirection;
import openmods.api.ICustomHarvestDrops;
import openmods.api.IPlaceAwareTile;
import openmods.include.IncludeInterface;
import openmods.include.IncludeOverride;
import openmods.network.event.NetworkEventManager;
import openmods.network.senders.ITargetedPacketSender;
import openmods.tileentity.OpenTileEntity;
import openmods.utils.ItemUtils;
import openperipheral.addons.glasses.GlassesEvent.GlassesClientEvent;
import openperipheral.addons.glasses.drawable.Drawable;
import openperipheral.addons.glasses.server.DrawableContainerMaster;
import openperipheral.addons.glasses.server.IDrawableFactory;
import openperipheral.addons.glasses.server.SurfaceServer;
import openperipheral.addons.glasses.server.TerminalManagerServer;
import openperipheral.api.adapter.Asynchronous;
import openperipheral.api.adapter.Doc;
import openperipheral.api.adapter.method.Arg;
import openperipheral.api.adapter.method.ReturnType;
import openperipheral.api.adapter.method.ScriptCallable;
import openperipheral.api.architecture.FeatureGroup;
import openperipheral.api.architecture.IArchitectureAccess;
import openperipheral.api.architecture.IAttachable;
import openperipheral.api.peripheral.PeripheralTypeId;

@Doc({ "This peripheral is used to control terminal glasses and wireless keyboard.",
		"There is one global surface and one private surface for every glasses user.",
		"All calls names .add*() will return object that can be later used to modify it.",
		"To make changes visible to players, call .sync().",
		"This peripheral signals few events. Full list available here: http://goo.gl/8Hf2yA",
		"Simple demo: http://goo.gl/n5HPN8" })
@PeripheralTypeId("openperipheral_bridge")
@FeatureGroup("openperipheral-glasses")
public class TileEntityGlassesBridge extends OpenTileEntity implements IAttachable, IPlaceAwareTile, ICustomHarvestDrops, IClearable {

	private static final String GLOBAL_FAKE_PLAYER_NAME = "$GLOBAL$";

	public static final String TAG_GUID = "guid";

	private static final String EVENT_PLAYER_ATTACH = "glasses_attach";

	private static final String EVENT_PLAYER_DETACH = "glasses_detach";

	private static class PlayerInfo {
		public final GameProfile profile;
		public final WeakReference<EntityPlayerMP> player;
		public final SurfaceServer surface;

		public PlayerInfo(long guid, EntityPlayerMP player) {
			this.player = new WeakReference<EntityPlayerMP>(player);
			this.profile = player.getGameProfile();
			this.surface = SurfaceServer.createPrivateSurface(guid);
		}
	}

	private final Map<UUID, PlayerInfo> knownPlayersByUUID = Maps.newHashMap();
	private final Map<String, PlayerInfo> knownPlayersByName = Maps.newHashMap();
	private List<Object> globalFullDataPackets;

	private Set<IArchitectureAccess> computers = Sets.newIdentityHashSet();

	private Optional<Long> guid = Optional.absent();

	private SurfaceServer globalSurface;

	@IncludeInterface(IContainer.class)
	private IContainer<Drawable> getDrawablesContainer() {
		return globalSurface.drawablesContainer;
	}

	@IncludeInterface(IDrawableFactory.class)
	private IDrawableFactory getDrawablesFactory() {
		return globalSurface.drawablesFactory;
	}

	public TileEntityGlassesBridge() {}

	private void rebuildPlayerNamesMap() {
		knownPlayersByName.clear();

		for (PlayerInfo info : knownPlayersByUUID.values()) {
			final EntityPlayerMP player = info.player.get();
			if (isPlayerValid(player)) {
				String name = player.getGameProfile().getName();
				knownPlayersByName.put(name, info);
			}
		}
	}

	public long getOrCreateGuid() {
		if (this.guid.isPresent())
			return this.guid.get();

		final long newGuid = TerminalUtils.generateGuid();
		this.guid = Optional.of(newGuid);
		return newGuid;
	}

	public void registerTerminal(EntityPlayerMP player) {
		if (!knownPlayersByUUID.containsKey(player.getGameProfile().getId())) {
			final PlayerInfo playerInfo = new PlayerInfo(getOrCreateGuid(), player);
			final GameProfile gameProfile = player.getGameProfile();

			knownPlayersByUUID.put(gameProfile.getId(), playerInfo);
			rebuildPlayerNamesMap();
			queueEvent(EVENT_PLAYER_ATTACH, player);

			sentStoredFullDataToPlayer(player);
		}
	}

	private void sentStoredFullDataToPlayer(EntityPlayer player) {
		if (globalFullDataPackets != null) NetworkEventManager.INSTANCE.dispatcher().senders.player.sendMessages(globalFullDataPackets, player);
	}

	private static interface IEventArgsSource {
		public Object[] getArgs(IArchitectureAccess access);
	}

	private void queueEvent(String event, EntityPlayer user, IEventArgsSource source) {
		final GameProfile gameProfile = user.getGameProfile();
		final UUID userId = gameProfile.getId();
		final String idString = userId != null? userId.toString() : null;
		final String userName = gameProfile.getName();

		for (IArchitectureAccess computer : computers) {
			final Object[] extra = source.getArgs(computer);
			final Object[] args = new Object[3 + extra.length];
			System.arraycopy(extra, 0, args, 3, extra.length);
			args[0] = computer.peripheralName();
			args[1] = userName;
			args[2] = idString;

			computer.signal(event, args);
		}
	}

	private void queueEvent(String event, EntityPlayer user, final Object... args) {
		queueEvent(event, user, new IEventArgsSource() {
			@Override
			public Object[] getArgs(IArchitectureAccess access) {
				return args;
			}
		});
	}

	public void onChatCommand(String event, String content, EntityPlayer player) {
		queueEvent(event, player, content);
	}

	@Override
	public void updateEntity() {
		super.updateEntity();
		if (worldObj.isRemote) return;

		final long guid = getOrCreateGuid();

		if (globalSurface == null) globalSurface = SurfaceServer.createPublicSurface(guid);
		TerminalManagerServer.instance.registerBridge(guid, this);

		boolean playersRemoved = false;
		Iterator<PlayerInfo> it = knownPlayersByUUID.values().iterator();
		while (it.hasNext()) {
			final PlayerInfo info = it.next();
			final EntityPlayerMP player = info.player.get();

			if (!isPlayerValid(player)) {
				queueEvent(EVENT_PLAYER_DETACH, player);
				sendClearPacketToPlayer(player, globalSurface);
				sendClearPacketToPlayer(player, info.surface);
				it.remove();
				playersRemoved = true;
			}
		}

		if (playersRemoved) rebuildPlayerNamesMap();
	}

	private static void sendClearPacketToPlayer(EntityPlayerMP player, SurfaceServer surface) {
		surface.drawablesContainer.createClearPacket().sendToPlayer(player);
	}

	private boolean isPlayerValid(EntityPlayerMP player) {
		if (player == null) return false;

		if (player.isDead && !isPlayerLogged(player)) return false;

		final Optional<Long> guid = TerminalIdAccess.instance.getIdFrom(player);
		return guid.isPresent() && guid.get() == getOrCreateGuid();
	}

	private static boolean isPlayerLogged(EntityPlayerMP player) {
		final GameProfile gameProfile = player.getGameProfile();
		@SuppressWarnings("unchecked")
		List<EntityPlayerMP> players = MinecraftServer.getServer().getConfigurationManager().playerEntityList;
		for (EntityPlayerMP p : players) {
			if (p.getGameProfile().equals(gameProfile)) return true;
		}

		return false;
	}

	@Override
	public void writeToNBT(NBTTagCompound tag) {
		super.writeToNBT(tag);
		if (guid.isPresent()) tag.setLong(TAG_GUID, guid.get());
	}

	@Override
	public void readFromNBT(NBTTagCompound tag) {
		super.readFromNBT(tag);
		this.guid = Optional.fromNullable(TerminalUtils.extractGuid(tag));
	}

	@Override
	public void onBlockPlacedBy(EntityPlayer player, ForgeDirection side, ItemStack stack, float hitX, float hitY, float hitZ) {
		NBTTagCompound tag = stack.getTagCompound();
		if (tag != null && tag.hasKey(TAG_GUID)) this.guid = Optional.of(tag.getLong(TAG_GUID));
	}

	@Override
	public void addHarvestDrops(EntityPlayer player, List<ItemStack> drops) {
		ItemStack result = new ItemStack(getBlockType());
		if (guid.isPresent()) {
			NBTTagCompound tag = ItemUtils.getItemTag(result);
			tag.setLong(TAG_GUID, guid.get());
		}
		drops.add(result);
	}

	@Override
	public boolean suppressNormalHarvestDrops() {
		return true;
	}

	@Override
	public void addComputer(IArchitectureAccess computer) {
		if (!computers.contains(computer)) {
			computers.add(computer);
		}
	}

	@Override
	public void removeComputer(IArchitectureAccess computer) {
		computers.remove(computer);
	}

	public void handleUserEvent(final GlassesClientEvent evt) {
		queueEvent(evt.getEventName(), evt.sender, new IEventArgsSource() {
			@Override
			public Object[] getArgs(IArchitectureAccess access) {
				return evt.getEventArgs(access);
			}
		});
	}

	public SurfaceServer getSurface(String username) {
		if (GLOBAL_FAKE_PLAYER_NAME.equals(username)) return globalSurface;
		PlayerInfo info = knownPlayersByName.get(username);
		return info != null? info.surface : null;
	}

	public SurfaceServer getSurface(UUID uuid) {
		if (TerminalUtils.GLOBAL_SURFACE_UUID.equals(uuid)) return globalSurface;
		PlayerInfo info = knownPlayersByUUID.get(uuid);
		return info != null? info.surface : null;
	}

	// never, ever make this asynchronous
	@ScriptCallable(description = "Send updates to client. Without it changes won't be visible", name = "sync")
	public void syncContents() {
		final DrawableContainerMaster drawables = globalSurface.drawablesContainer;
		synchronized (drawables) {
			final boolean globalChanged = drawables.hasUpdates();

			if (globalChanged || globalFullDataPackets == null) {
				final TerminalEvent.Data globalFullData = drawables.createFullDataEvent();
				globalFullDataPackets = globalFullData.serialize();
			}

			List<Object> globalUpdateDataPackets = null;

			if (globalChanged) {
				final TerminalEvent.Data globalDelta = drawables.createUpdateDataEvent();
				globalUpdateDataPackets = globalDelta.serialize();
			}

			final ITargetedPacketSender<EntityPlayer> playerSender = NetworkEventManager.INSTANCE.dispatcher().senders.player;

			for (PlayerInfo info : knownPlayersByUUID.values()) {
				final EntityPlayerMP player = info.player.get();
				if (isPlayerValid(player)) {
					if (globalUpdateDataPackets != null) playerSender.sendMessages(globalUpdateDataPackets, player);
					sendPrivateUpdateToPlayer(player, info);
				}
			}
		}
	}

	private static void sendPrivateUpdateToPlayer(EntityPlayerMP player, PlayerInfo info) {
		final SurfaceServer privateSurface = info.surface;
		final DrawableContainerMaster drawables = privateSurface.drawablesContainer;
		synchronized (drawables) {
			if (drawables.hasUpdates()) {
				final TerminalEvent.Data privateUpdateDataPackets = drawables.createUpdateDataEvent();
				privateUpdateDataPackets.sendToPlayer(player);
			}
		}
	}

	private static void sendFullDataPacketToPlayer(EntityPlayer player, final SurfaceServer surface) {
		surface.drawablesContainer.createFullDataEvent().sendToPlayer(player);
	}

	private void sendPrivateFullDataToPlayer(EntityPlayer player) {
		UUID playerUuid = player.getGameProfile().getId();
		PlayerInfo info = knownPlayersByUUID.get(playerUuid);
		if (info != null) sendFullDataPacketToPlayer(player, info.surface);
	}

	public void handlePrivateDrawableResetRequest(TerminalEvent.PrivateDrawableReset evt) {
		sendPrivateFullDataToPlayer(evt.sender);
	}

	public void handlePublicDrawableResetRequest(TerminalEvent.PublicDrawableReset evt) {
		sentStoredFullDataToPlayer(evt.sender);
	}

	@Asynchronous
	@ScriptCallable(returnTypes = ReturnType.TABLE, description = "Get the names of all the users linked up to this bridge")
	public List<GameProfile> getUsers() {
		List<GameProfile> result = Lists.newArrayList();
		for (PlayerInfo info : knownPlayersByUUID.values())
			result.add(info.profile);

		return result;
	}

	@Asynchronous
	@ScriptCallable(returnTypes = ReturnType.STRING, name = "getGuid", description = "Get the Guid of this bridge")
	public String getGuidString() {
		return TerminalUtils.formatTerminalId(getOrCreateGuid());
	}

	@Override
	@IncludeOverride
	public void clear() {
		globalSurface.drawablesContainer.clear();

		for (PlayerInfo info : knownPlayersByUUID.values())
			info.surface.drawablesContainer.clear();
	}

	@Asynchronous
	@ScriptCallable(returnTypes = ReturnType.OBJECT, description = "Get the surface of a user to draw privately on their screen")
	public SurfaceServer getSurfaceByName(@Arg(name = "username", description = "The username of the user to get the draw surface for") String username) {
		SurfaceServer playerSurface = getSurface(username);
		Preconditions.checkNotNull(playerSurface, "Invalid player");
		return playerSurface;
	}

	@Asynchronous
	@ScriptCallable(returnTypes = ReturnType.OBJECT, description = "Get the surface of a user to draw privately on their screen")
	public SurfaceServer getSurfaceByUUID(@Arg(name = "uuid", description = "The uuid of the user to get the draw surface for") UUID uuid) {
		SurfaceServer playerSurface = getSurface(uuid);
		Preconditions.checkNotNull(playerSurface, "Invalid player");
		return playerSurface;
	}

	@Asynchronous
	@ScriptCallable(returnTypes = ReturnType.OBJECT, description = "Returns object used for controlling player capture mode")
	public GuiCaptureControl getCaptureControl(@Arg(name = "uuid") UUID uuid) {
		PlayerInfo info = knownPlayersByUUID.get(uuid);
		return info != null? new GuiCaptureControl(getOrCreateGuid(), info.player) : null;
	}
}