package fr.insa.clavardator.client.users;

import fr.insa.clavardator.client.chat.ChatHistory;
import fr.insa.clavardator.client.db.DatabaseController;
import fr.insa.clavardator.client.server.proxy.Proxy;
import fr.insa.clavardator.lib.errors.UsernameTakenException;
import fr.insa.clavardator.lib.message.FileMessage;
import fr.insa.clavardator.lib.message.Message;
import fr.insa.clavardator.lib.network.TcpConnection;
import fr.insa.clavardator.lib.users.User;
import fr.insa.clavardator.lib.users.UserInformation;
import fr.insa.clavardator.lib.users.UserState;
import fr.insa.clavardator.lib.util.ErrorCallback;
import fr.insa.clavardator.lib.util.Log;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.Date;

public class PeerUser extends User implements Comparable<PeerUser> {
	private final transient ChatHistory history;
	private UserState state = UserState.DISCONNECTED;
	private PeerConnection connection;

	public PeerUser(String id, String username) {
		super(id, username);
		history = new ChatHistory(this);
	}

	public PeerUser() {
		super();
		history = new ChatHistory(this);
	}

	private void init(String id, String username, ErrorCallback errorCallback) {
		setId(id);
		setUsername(username);
		setState(UserState.CONNECTED);
		connection.receive(msg -> onMessageReceived(msg, errorCallback), e -> {
			disconnect();
			if (!(e instanceof EOFException)) {
				Log.e(this.getClass().getSimpleName(), "Error receiving message from " + getId(), e);
				errorCallback.onError(e);
			}
		});
	}

	public void init(TcpConnection tcpConnection, String id, String username, ErrorCallback errorCallback) {
		connection = new DirectPeerConnection(tcpConnection);
		init(id, username, errorCallback);
	}

	public void init(Proxy proxy, String id, String username, ErrorCallback errorCallback) {
		connection = new ProxyPeerConnection(proxy, getId());
		init(id, username, errorCallback);
	}


	/**
	 * Sends a basic text message to this user
	 *
	 * @param msg           The text message to send
	 * @param errorCallback Callback on error
	 */
	public void sendTextMessage(String msg, @Nullable ErrorCallback errorCallback) {
		if (connection != null) {
			Log.v(this.getClass().getSimpleName(),
					"Sending message to " + getUsername() + " / " + getId() + ": " + msg);
			final Message message = new Message(CurrentUser.getInstance(), this, new Date(), msg);
			connection.send(message, () -> addMessageToHistory(message, errorCallback), errorCallback);
		} else {
			Log.e(this.getClass().getSimpleName(), "Could not send message: connection is not initialized");
		}
	}


	/**
	 * Sends a message containing a file to this user
	 *
	 * @param msg           The text message to send
	 * @param errorCallback Callback on error
	 */
	public void sendFileMessage(String msg, File file, @Nullable ErrorCallback errorCallback) {
		if (connection != null) {
			Log.v(this.getClass().getSimpleName(),
					"Sending file message to " + this.getUsername() + " / " + this.getId() + ": " + msg);
			try {
				final FileMessage message = new FileMessage(CurrentUser.getInstance(), this, new Date(), msg, file.getPath());
				message.storeFile();
				connection.send(message, () -> addMessageToHistory(message, errorCallback), errorCallback);
			} catch (IOException e) {
				Log.e(this.getClass().getSimpleName(), "Could not send message: error while opening file", e);
				if (errorCallback != null) {
					errorCallback.onError(e);
				}
			}
		} else {
			Log.e(this.getClass().getSimpleName(), "Could not send message: connection is not initialized");
		}
	}

	/**
	 * Sends current user information to this user
	 *
	 * @param errorCallback Callback on error
	 */
	public void sendCurrentUser(@Nullable ErrorCallback errorCallback) {
		if (connection != null) {
			final String username = CurrentUser.getInstance().getUsername();
			Log.v(this.getClass().getSimpleName(),
					"Sending current user information to " + this.getUsername() + " / " + this.getId() + ": " + username);
			connection.send(new UserInformation(CurrentUser.getInstance()), null, errorCallback);
		} else {
			Log.e(this.getClass().getSimpleName(), "Could not send new username: connection is not initialized");
		}
	}

	protected void sendUsernameTaken() {
		if (connection != null) {
			Log.v(this.getClass().getSimpleName(), "Received username request using current username");
			UsernameTakenException exception = new UsernameTakenException("Username taken",
					CurrentUser.getInstance().getId(), getId());
			connection.send(exception, null, null);
		} else {
			Log.e(this.getClass().getSimpleName(), "Could not send UsernameTaken: connection is not initialized");
		}
	}

	protected void addMessageToHistory(Message message, @Nullable ErrorCallback errorCallback) {
		history.addMessage(message, errorCallback);
	}


	protected void onMessageReceived(Object msg, ErrorCallback errorCallback) throws IOException {
		Log.v(this.getClass().getSimpleName(), "Received message from " + getId());
		if (msg instanceof UserInformation) {
			assert ((UserInformation) msg).id.equals(getId());
			final String receivedUsername = ((UserInformation) msg).getUsername();
			Log.v(this.getClass().getSimpleName(), "Message username: " + receivedUsername);
			if (CurrentUser.getInstance().getUsername().equals(receivedUsername)) {
				sendUsernameTaken();
			} else {
				setUsername(receivedUsername);
			}
		} else if (msg instanceof Message) {
			assert !((Message) msg).getRecipient().id.equals(getId());

			Log.v(this.getClass().getSimpleName(), "Message text: " + ((Message) msg).getText());
			if (msg instanceof FileMessage) {
				((FileMessage) msg).storeFile();
			}
			history.addMessage((Message) msg, errorCallback);
		} else if (msg instanceof UsernameTakenException) {
			CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
			errorCallback.onError(new Exception("Received username already taken message"));
		}
	}

	public void disconnect() {
		if (connection != null) {
			Log.v(this.getClass().getSimpleName(), "Disconnecting from user: " + getId());
			connection.disconnect();
			connection = null;
		}
		setState(UserState.DISCONNECTED);
	}

	/**
	 * Gets the value of history
	 *
	 * @return the value of history
	 */
	public ChatHistory getHistory() {
		return history;
	}

	/**
	 * Sets this user state
	 *
	 * @param state The new state
	 */
	protected void setState(UserState state) {
		getPcs().firePropertyChange("state", this.state, state);
		this.state = state;
	}

	@Override
	protected void setUsername(String newUsername) {
		super.setUsername(newUsername);
		final DatabaseController db = new DatabaseController();
		db.updateUsername(new UserInformation(this),
				null,
				e -> Log.e(getClass().getSimpleName(), "Unable to update the username", e));
	}


	/**
	 * Check if this user is active.
	 *
	 * @return True id active, false otherwise
	 */
	public boolean isActive() {
		return state == UserState.CONNECTED;
	}

	@Override
	public int compareTo(@NotNull PeerUser peerUser) {
		if (peerUser.isActive() && !this.isActive()) {
			return 1;
		} else if (!peerUser.isActive() && this.isActive()) {
			return -1;
		}
		return getUsername().compareTo(peerUser.getUsername());
	}
}
