package de.beyondsoft.ownchat.page.chat;

import de.beyondsoft.ownchat.R;
import de.beyondsoft.ownchat.data.AppPrefsConstants;
import de.beyondsoft.ownchat.data.api.common.ErrorHandlingResponseConverter;
import de.beyondsoft.ownchat.data.api.responses.MessageResponse;
import de.beyondsoft.ownchat.data.api.services.ChatService;
import de.beyondsoft.ownchat.data.events.GroupUpdatedEvent;
import de.beyondsoft.ownchat.data.prefs.LongPreference;
import de.beyondsoft.ownchat.data.prefs.StringPreference;
import de.beyondsoft.ownchat.data.repository.GroupRealmRepository;
import de.beyondsoft.ownchat.data.repository.MessageRealmRepository;
import de.beyondsoft.ownchat.data.repository.RealmProvider;
import de.beyondsoft.ownchat.data.repository.models.MessageRealm;
import de.beyondsoft.ownchat.data.repository.specifications.GroupByIdSpecification;
import de.beyondsoft.ownchat.data.repository.specifications.MessageByGroupIdSpecification;
import de.beyondsoft.ownchat.data.repository.specifications.MessageByIdSpecification;
import de.beyondsoft.ownchat.data.repository.specifications.MessagesByGroupAndSearchTextSpecification;
import de.beyondsoft.ownchat.di.scopes.ActivityScope;
import de.beyondsoft.ownchat.model.Group;
import de.beyondsoft.ownchat.model.Message;
import de.beyondsoft.ownchat.page.mvp.MVPAbstractPresenter;
import de.beyondsoft.ownchat.utils.BeyondSoftLog;
import de.beyondsoft.ownchat.utils.Constants;
import de.beyondsoft.ownchat.utils.SystemUtils;
import de.beyondsoft.ownchat.utils.Utils;
import de.beyondsoft.ownchat.utils.rx.RxUtils;

import org.greenrobot.eventbus.EventBus;

import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.util.Pair;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;

import javax.inject.Inject;
import javax.inject.Named;

import de.beyondsoft.ownchat.utils.FileUtils;
import io.realm.Realm;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import rx.Completable;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;

import static de.beyondsoft.ownchat.data.AppPrefsConstants.USER_ID;

@ActivityScope
public class ChatPresenter extends MVPAbstractPresenter<ChatView> implements ChatView.Callback {

    private static final int FIRST_PAGE = 1;
    private static final int GROUP_ID_NOT_SET = -999;
    private static final int LIMIT = 25;

    @Inject
    ChatService mChatService;

    @Inject
    MessageRealmRepository mMessageRealmRepository;

    @Inject
    GroupRealmRepository mGroupRealmRepository;

    @Inject
    SystemUtils mSystemUtils;

    @Inject
    @Named(USER_ID)
    LongPreference mUserIdPreference;

    @Inject
    @Named(AppPrefsConstants.AVATAR)
    StringPreference mAvatarPreference;

    @Inject
    @Named(AppPrefsConstants.NAME)
    StringPreference mProfileNamePreference;

    @Inject
    @Named(AppPrefsConstants.UPLOAD_MAX_SIZE)
    LongPreference mUploadMaxSizePreference;

    @Inject
    @Named(AppPrefsConstants.VERIFY_CHAT_CODE_ERROR_MESSAGE)
    StringPreference mVerifyChatCodeErrorMessage;

    @Inject
    RealmProvider mRealmProvider;

    @Inject
    Context mContext;

    @Inject
    ChatPresenter() {
    }

    private long mGroupId = GROUP_ID_NOT_SET;
    private boolean mLoading = false;
    private int mPage = FIRST_PAGE;
    private int mOffset;
    private boolean mFetchMore;
    private ArrayList<Message> mMessages = new ArrayList<>();
    private ArrayList<Pair<Message, String>> mMessageToUpdate = new ArrayList<>();
    private int mDownwardsOffset = -1;

    public int getPage() {
        return mPage;
    }

    private boolean mIsScrollingDown = false;
    public void setIsScrollingDown(boolean isScrollingDown) {
        mIsScrollingDown = isScrollingDown;
    }

    public boolean getIsScrollingDown() {
        return mIsScrollingDown;
    }

    /**
     * Get all messages from the server with a specific groupId.
     */
    @Override
    public void loadMessagesFromServerByGroupId(long groupId) {
        if(!mFetchMore) {
            getView().hideMessagesLoading();
            return;
        }

        mGroupId = groupId;

        Subscription subscription = mChatService.getMessages(groupId, mPage)
                .compose(RxUtils.provideDefaultTransformer())
                .flatMap(new ErrorHandlingResponseConverter<>())
                .flatMap(messageResponse -> {
                    ArrayList<Message> messages = messageResponse.response.messages;

                    for(int i = 0, size = messages.size(); i < size; i++) {
                        messages.get(i).status = Message.Status.DELIVERED;
                        messages.get(i).isRead = true;
                        messages.get(i).userId = mUserIdPreference.get();
                    }

                    mFetchMore = messageResponse.response.hasMorePages;

                    return Observable.just(messageResponse.response.messages);
                })
                .subscribe(this::messagesSuccess, this::messagesError);
        addSubscription(subscription);
    }

    /**
     * Get messages from page 0.
     */
    @Override
    public void fetchNewMessages(long groupId) {
        mGroupId = groupId;
        Subscription subscription = mChatService.getMessages(mGroupId, 0)
                .compose(RxUtils.provideDefaultTransformer())
                .flatMap(new ErrorHandlingResponseConverter<>())
                .flatMap(messageResponse -> {
                    if(messageResponse.response.messages == null || messageResponse.response.messages.isEmpty()) {
                        return Observable.just(null);
                    }

                    Message lastMessage = messageResponse.response.messages.get(messageResponse.response.messages.size() - 1);
                    lastMessage.status = Message.Status.DELIVERED;
                    lastMessage.isRead = true;

                    if(!mMessages.contains(lastMessage)) {
                        long groupId1 = mGroupId;
                        resetMessages();
                        loadMessagesFromServerByGroupId(groupId1);

                        return Observable.just(null);
                    }

                    for(int i = 0; i < mMessageToUpdate.size(); i++) {
                        Pair<Message, String> messageToUpdate = mMessageToUpdate.get(i);

                        if(isAttached()) {
                            getView().lastMessageUpdated(messageToUpdate.first, messageToUpdate.second);
                        }
                    }

                    mMessageToUpdate.clear();
                    ArrayList<Message> messages = new ArrayList<>();

                    for(int i = messageResponse.response.messages.size() - 1; i >= 0; i--) {
                        Message message = messageResponse.response.messages.get(i);
                        message.status = Message.Status.DELIVERED;
                        message.isRead = true;
                        message.userId = mUserIdPreference.get();

                        if(mMessages.contains(message)) {
                            continue;
                        }

                        messages.add(message);
                    }

                    return mMessageRealmRepository.updateMessages(messages, new MessageByIdSpecification(mUserIdPreference.get()));
                })
                .subscribe(this::fetchNewMessagesSuccess, this::fetchNewMessagesError);
        addSubscription(subscription);
    }

    /**
     * Send messages to view.
     */
    private void fetchNewMessagesSuccess(List<Message> messages) {
        if(messages == null) {
            return;
        }

        for(int i = 0; i < messages.size(); i++) {
            if(!isAttached()) {
                break;
            }

            Message message = messages.get(i);
            mMessages.add(message);

            getView().addOneMessage(message);
        }
    }

    /**
     * Send error messages to view.
     */
    private void fetchNewMessagesError(Throwable throwable) {
        Log.e("fetchNewMessages", "onError", throwable);

        if(!isAttached()) {
            return;
        }

        if(mSystemUtils.isNetworkUnavailable()) {
            getView().showError(R.string.error_no_internet_connection);
            return;
        }

        getView().showError(R.string.error_something_went_wrong);
    }

    @Override
    public void resetMessages() {
        mGroupId = GROUP_ID_NOT_SET;
        mLoading = false;
        mPage = FIRST_PAGE;
        mOffset = 0;
        mFetchMore = true;
        mDownwardsOffset = -1;
    }

    /**
     * Save messages to local database.
     */
    private void messagesSuccess(ArrayList<Message> messages) {
        mMessageRealmRepository.update(messages, new MessageByIdSpecification(mUserIdPreference.get()))
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<Object>() {
                    @Override
                    public void onCompleted() {
                        loadMessagesFromDBByGroupId(mGroupId, true);
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(ChatPresenter.class.getSimpleName(), e.getMessage(), e);
                    }

                    @Override
                    public void onNext(Object o) {
                        // nop here
                    }
                });
    }

    private void messagesError(Throwable throwable) {
        Log.e(ChatPresenter.class.getSimpleName(), throwable.getMessage(), throwable);
        getView().hideLoading();
        getView().hideMessagesLoading();

        if(mSystemUtils.isNetworkUnavailable()) {
            getView().showError(R.string.error_no_internet_connection);
            return;
        }

        getView().showError(R.string.error_something_went_wrong);
    }

    @Override
    public void loadMessagesFromDBByGroupId(long groupId, boolean fetchWhenFinished) {
        mGroupId = groupId;
        loadMessagesFromDBByGroupId();
    }

    /**
     * Get messages from database using a specific groupId.
     */
    private void loadMessagesFromDBByGroupId() {
        MessageByGroupIdSpecification messageByGroupIdSpecification = new MessageByGroupIdSpecification(mUserIdPreference.get());
        messageByGroupIdSpecification.setGroupId(mGroupId);

        mMessageRealmRepository.queryForAll(messageByGroupIdSpecification)
                .map(ArrayList::new)
                .compose(RxUtils.provideDefaultTransformer())
                .subscribe(this::loadMessagesFromDBSuccessful, this::loadMessagesError);
    }

    private String mText;
    public void searchMessagesFromDBByGroupIdAndText(long groupId, String text) {
        MessagesByGroupAndSearchTextSpecification messagesByGroupAndSearchTextSpecification = new MessagesByGroupAndSearchTextSpecification(mUserIdPreference.get());
        messagesByGroupAndSearchTextSpecification.setGroupId(groupId);
        messagesByGroupAndSearchTextSpecification.setSearchText(text);

        mGroupId = groupId;
        mText = text;

        mMessageRealmRepository.queryForAll(messagesByGroupAndSearchTextSpecification)
                .map(ArrayList::new)
                .compose(RxUtils.provideDefaultTransformer())
                .subscribe(this::searchMessagesFromDBSuccessful, this::loadMessagesError);
    }

    private void searchMessagesFromDBSuccessful(ArrayList<Message> messages) {
        if(!isAttached()) {
            return;
        }

        mMessages.clear();
        mMessages.addAll(messages);
        mLoading = false;

        getView().hideLoading();
        getView().hideMessagesLoading();

        if(!mMessages.isEmpty()) {
            int endOffset = mOffset + LIMIT > mMessages.size() ? mMessages.size() : mOffset + LIMIT;
            if(mOffset > endOffset) {
                return;
            }

            boolean isFirstPage = mPage == FIRST_PAGE;

            getView().messagesLoaded(new ArrayList<>(mMessages.subList(mOffset, endOffset)), isFirstPage);

            if(isFirstPage && mSystemUtils.isNetworkUnavailable()) {
                Message firstMessage = mMessages.get(0);
                Message message = new Message(firstMessage.id,
                        firstMessage.groupId,
                        firstMessage.createdAt / 1000,
                        firstMessage.message,
                        firstMessage.filePath,
                        firstMessage.smallerImage,
                        firstMessage.originalFilename,
                        firstMessage.uri,
                        firstMessage.senderId,
                        firstMessage.senderName,
                        firstMessage.senderAvatar,
                        firstMessage.status,
                        true,
                        firstMessage.userId,
                        firstMessage.fileDownloaded);
                storeGroupLocally(mGroupId, message);
            }

            mPage++;
            mOffset += LIMIT;

            return;
        }

        getView().showEmptySearchResultMessage(mText);
    }

    private Message mMessageToScrollTo;
    public void loadMessagesInContext(long groupId, long createdAt, Message messageToScrollTo) {
        MessageByGroupIdSpecification messageByGroupIdSpecification = new MessageByGroupIdSpecification(mUserIdPreference.get());
        messageByGroupIdSpecification.setGroupId(groupId);

        mGroupId = groupId;
        mMessageToScrollTo = messageToScrollTo;

        mMessageRealmRepository.queryForAll(messageByGroupIdSpecification)
                .map(ArrayList::new)
                .compose(RxUtils.provideDefaultTransformer())
                .subscribe(this::loadMessagesInContextSuccessful, this::loadMessagesError);
    }

    private void loadMessagesInContextSuccessful(ArrayList<Message> messages) {
        if(!isAttached()) {
            return;
        }

        mMessages.clear();
        mMessages.addAll(messages);
        mLoading = false;

        getView().hideLoading();
        getView().hideMessagesLoading();

        if(!mMessages.isEmpty()) {
            int tempLimit = (LIMIT - 1) / 2;

            int position = 0;
            mOffset = 0;
            for(Message message : mMessages) {
                if(message.equals(mMessageToScrollTo)) {
                    position = mMessages.indexOf(message);
                }
            }

            mOffset = position < tempLimit ? 0 : position - tempLimit;

            int endOffset = mOffset + LIMIT > mMessages.size() ? mMessages.size() : mOffset + LIMIT;

            if(mOffset > endOffset) {
                return;
            }

            double pos = (double) position;
            double lim = (double) LIMIT;

            mPage = (int) Math.ceil(pos / lim);
            boolean isFirstPage = mPage == FIRST_PAGE;

            getView().messagesLoaded(new ArrayList<>(mMessages.subList(mOffset, endOffset)));

            if(isFirstPage && mSystemUtils.isNetworkUnavailable()) {
                Message firstMessage = mMessages.get(0);
                Message message = new Message(firstMessage.id,
                        firstMessage.groupId,
                        firstMessage.createdAt / 1000,
                        firstMessage.message,
                        firstMessage.filePath,
                        firstMessage.smallerImage,
                        firstMessage.originalFilename,
                        firstMessage.uri,
                        firstMessage.senderId,
                        firstMessage.senderName,
                        firstMessage.senderAvatar,
                        firstMessage.status,
                        true,
                        firstMessage.userId,
                        firstMessage.fileDownloaded);
                storeGroupLocally(mGroupId, message);
            }

            mOffset += LIMIT;

            getView().scrollToMessage(mMessageToScrollTo);

            return;
        }

        getView().showEmptyMessage();
    }

    /**
     * Send messages to view.
     */
    private void loadMessagesFromDBSuccessful(ArrayList<Message> messages) {
        if(!isAttached()) {
            return;
        }

        mMessages.clear();
        mMessages.addAll(messages);
        mLoading = false;

        getView().hideLoading();
        getView().hideMessagesLoading();

        if(!mMessages.isEmpty()) {
            int offsetTmp, endOffsetTmp;

            int endOffset = mOffset + LIMIT > mMessages.size() ? mMessages.size() : mOffset + LIMIT;

            if(mIsScrollingDown) {
                if(mDownwardsOffset < 0) {
                    mDownwardsOffset = mOffset >= (LIMIT * 2) ? mOffset - (LIMIT * 2) : 0;
                } else {
                    mDownwardsOffset = mDownwardsOffset >= (LIMIT * 2) ? mDownwardsOffset - (LIMIT * 2) : 0;
                }

                int downwardsEndOffset = mDownwardsOffset + (LIMIT * 2) > mMessages.size() ? mMessages.size() : mDownwardsOffset + (LIMIT * 2);

                offsetTmp = mDownwardsOffset;
                endOffsetTmp = downwardsEndOffset;
            } else {
                offsetTmp = mOffset;
                endOffsetTmp = endOffset;
            }

            if(mOffset > endOffset) {
                Utils.makeScrollLog("Offset is greater than endOffset: " + mOffset + " > " + endOffset);
                return;
            }

            boolean isFirstPage = mPage == FIRST_PAGE;

            getView().messagesLoaded(new ArrayList<>(mMessages.subList(offsetTmp, endOffsetTmp)), isFirstPage);

            if(isFirstPage && mSystemUtils.isNetworkUnavailable()) {
                Message firstMessage = mMessages.get(0);
                Message message = new Message(firstMessage.id,
                        firstMessage.groupId,
                        firstMessage.createdAt / 1000,
                        firstMessage.message,
                        firstMessage.filePath,
                        firstMessage.smallerImage,
                        firstMessage.originalFilename,
                        firstMessage.uri,
                        firstMessage.senderId,
                        firstMessage.senderName,
                        firstMessage.senderAvatar,
                        firstMessage.status,
                        true,
                        firstMessage.userId,
                        firstMessage.fileDownloaded);
                storeGroupLocally(mGroupId, message);
            }

            mPage++;

            if(!mIsScrollingDown) {
                mOffset += LIMIT;
            }

            return;
        }

        getView().showEmptyMessage();
    }

    private void loadMessagesError(Throwable throwable) {
        if(!isAttached()) {
            return;
        }

        mLoading = false;
        getView().hideLoading();
        getView().hideMessagesLoading();
    }

    /**
     * If mMessages size is bigger then 25 and network is available, get more messages from the server.
     */
    @Override
    public void fetchMoreMessagesByGroupId() {
        if(mMessages.size() < 25) {
            getView().hideMessagesLoading();

            return;
        }

        mLoading = true;

        if(mPage == FIRST_PAGE) {
            getView().showLoading();
        } else {
            getView().showMessagesLoading();
        }

        if(mSystemUtils.isNetworkUnavailable()) {
            loadMessagesFromDBByGroupId();
            getView().showError(R.string.error_no_internet_connection);

            return;
        }

        loadMessagesFromServerByGroupId(mGroupId);
    }

    @Override
    public boolean isLoading() {
        return mLoading;
    }

    /**
     * Create message with a specific file path, save it to the local database.
     * Send the file to the server.
     */
    @Override
    public void sendFile(final Uri fileUri, String messageText) {
        File file = getView().getFile(fileUri);

        final String mimeType = FileUtils.getMimeType(file);

        if(file == null || !file.exists()) {
            getView().showToastMessage(R.string.error_file_not_found);
            return;
        }

        if(mimeType != null && (mimeType.equals(Constants.MIME_TYPE_JPEG) || mimeType.equals(Constants.MIME_TYPE_PNG)) && (file.length() > (1000L * 1024L))) {
            FileUtils.clearTmpFilesDir(mContext);

            file = FileUtils.scaleImageDown(file, mContext);
        }

        if(mUploadMaxSizePreference.get() > 0 && file.length() > mUploadMaxSizePreference.get()) {
            getView().showError(R.string.error_file_is_too_large);
            return;
        }

        final File fileToSend = file;

        String[] split = fileToSend.getAbsolutePath().split("/");

        Message message = new Message(findUniqueRandomId(mRealmProvider, mUserIdPreference.get()),
                mGroupId,
                System.currentTimeMillis() / 1000,
                messageText,
                fileToSend.getPath(),
                fileToSend.getPath(),
                split[split.length - 1],
                fileUri.toString(),
                mUserIdPreference.get(),
                mProfileNamePreference.get(),
                mAvatarPreference.get(),
                Message.Status.PENDING,
                true,
                mUserIdPreference.get(),
                false);

        storeMessageLocally(message).subscribe(new Subscriber<Object>() {
            @Override
            public void onCompleted() {
                if(mSystemUtils.isNetworkUnavailable()) {
                    getView().showError(R.string.error_no_internet_connection);
                    message.status = Message.Status.FAILED;
                    message.isRead = true;
                    storeMessageLocally(message, message.id, true);
                    storeGroupLocally(mGroupId, message);
                    return;
                }

                checkForEmptyMessageAndHideIt();
                message.createdAt *= 1000;
                getView().addOneMessage(message);

                // create RequestBody instance from file
                RequestBody requestFile = RequestBody.create(getView().getMediaType(mimeType), fileToSend);

                // MultipartBody.Part is used to send also the actual file name
                MultipartBody.Part body = MultipartBody.Part.createFormData("file", fileToSend.getName(), requestFile);

                // add another part within the multipart request
                String groupIdString = String.valueOf(mGroupId);
                RequestBody groupIdBody = RequestBody.create(okhttp3.MultipartBody.FORM, groupIdString);

                RequestBody textBody = RequestBody.create(okhttp3.MultipartBody.FORM, message.message == null ? "" : message.message);

                mChatService.sendFile(groupIdBody, body, textBody)
                        .compose(RxUtils.provideDefaultTransformer())
                        .flatMap(new ErrorHandlingResponseConverter<>())
                        .subscribe(new Subscriber<MessageResponse>() {
                            @Override
                            public void onCompleted() {
                            }

                            @Override
                            public void onError(Throwable e) {
                                FileUtils.clearTmpFilesDir(mContext);
                                Log.e("sendFile", "onError", e);
                                @SuppressWarnings("UnnecessaryLocalVariable") Message newMessage = message;
                                newMessage.createdAt /= 1000;
                                newMessage.status = Message.Status.FAILED;
                                newMessage.isRead = true;
                                storeMessageLocally(newMessage, message.id, false);
                                storeGroupLocally(mGroupId, message);

                                if(!isAttached()) {
                                    return;
                                }

                                if(mSystemUtils.isNetworkUnavailable()) {
                                    getView().showError(R.string.error_no_internet_connection);
                                    return;
                                }

                                getView().showError(R.string.error_something_went_wrong);
                            }

                            @Override
                            public void onNext(MessageResponse baseResponse) {
                                FileUtils.clearTmpFilesDir(mContext);
                                deleteLocalMessage(message);
                                baseResponse.response.message.status = Message.Status.DELIVERED;
                                baseResponse.response.message.isRead = true;
                                storeMessageLocally(baseResponse.response.message, message.id, false);
                                storeGroupLocally(mGroupId, baseResponse.response.message);

                                List<String> splitUriAsList = Arrays.asList(fileUri.toString().split("/"));

                                if(splitUriAsList.contains(Constants.AUDIO_RECORDINGS_DIR)) {
                                    File f = getView().getFile(fileUri);
                                    if(f != null && f.exists()) {
                                        f.delete();
                                    }
                                }
                            }
                        });
            }

            @Override
            public void onError(Throwable e) {
                Log.e("sendFile", "onError", e);
            }

            @Override
            public void onNext(Object o) {

            }
        });
    }

    /**
     * Download file and notify the activity.
     */
    @Override
    public void downloadFile(Message message, String filePath, int itemPosition) {
        if(message.fileDownloaded) {
            getView().stopItemProgress(itemPosition);
            getView().fileDownloadedSuccessfully(filePath, FileUtils.getFileTypeFromFilePath(filePath));
            return;
        }

        downloadFileFromUrl(message.filePath, filePath, itemPosition)
                .compose(RxUtils.provideDefaultTransformer())
                .subscribe(new Subscriber<String>() {
                    @Override
                    public void onCompleted() {

                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("ChatPresenter", "downloadFile", e);

                        if(!isAttached()) {
                            return;
                        }

                        getView().stopItemProgress(itemPosition);

                        if(mSystemUtils.isNetworkUnavailable()) {
                            getView().showError(R.string.error_no_internet_connection);
                            return;
                        }

                        getView().showError(R.string.error_something_went_wrong);
                    }

                    @Override
                    public void onNext(String fileType) {
                        if(!isAttached()) {
                            return;
                        }

                        getView().stopItemProgress(itemPosition);
                        getView().fileDownloadedSuccessfully(filePath, fileType);

                        message.fileDownloaded = true;
                        message.createdAt /= 1000;

                        storeMessageLocally(message).subscribe();
                    }
                });
    }

    @Override
    public void resendMessage(Message message, String mimeType) {
        if(mSystemUtils.isNetworkUnavailable()) {
            getView().enableMessageRetry(message.id);
            getView().showError(R.string.error_no_internet_connection);
            return;
        }

        if(!TextUtils.isEmpty(message.filePath)) {
            Uri fileUri = Uri.parse(message.uri);
            File file = getView().getFile(fileUri);

            if(file == null || !file.exists()) {
                getView().showError(R.string.error_file_does_not_exist_anymore);
                return;
            }

            if(mimeType != null && (mimeType.equals(Constants.MIME_TYPE_JPEG) || mimeType.equals(Constants.MIME_TYPE_PNG)) && (file.length() > (1000L * 1024L))) {
                FileUtils.clearTmpFilesDir(mContext);

                file = FileUtils.scaleImageDown(file, mContext);
            }

            if(mUploadMaxSizePreference.get() > 0 && file.length() > mUploadMaxSizePreference.get()) {
                getView().showError(R.string.error_file_is_too_large);
                return;
            }

            final File fileToSend = file;

            // create RequestBody instance from file
            RequestBody requestFile = RequestBody.create(getView().getMediaType(fileUri), fileToSend);

            // MultipartBody.Part is used to send also the actual file name
            MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);

            // add another part within the multipart request
            String groupIdString = String.valueOf(mGroupId);
            RequestBody groupIdBody = RequestBody.create(okhttp3.MultipartBody.FORM, groupIdString);

            RequestBody textBody = RequestBody.create(MultipartBody.FORM, message.message == null ? "" : message.message);

            mChatService.sendFile(groupIdBody, body, textBody)
                    .compose(RxUtils.provideDefaultTransformer())
                    .flatMap(new ErrorHandlingResponseConverter<>())
                    .subscribe(new Subscriber<MessageResponse>() {
                        @Override
                        public void onCompleted() {
                        }

                        @Override
                        public void onError(Throwable e) {
                            FileUtils.clearTmpFilesDir(mContext);

                            Log.e("resendMessage", "onError", e);

                            if(!isAttached()) {
                                return;
                            }

                            getView().enableMessageRetry(message.id);

                            if(mSystemUtils.isNetworkUnavailable()) {
                                getView().showError(R.string.error_no_internet_connection);
                                return;
                            }

                            getView().showError(R.string.error_retry_failed);
                        }

                        @Override
                        public void onNext(MessageResponse baseResponse) {
                            FileUtils.clearTmpFilesDir(mContext);
                            deleteLocalMessage(message);
                            baseResponse.response.message.status = Message.Status.DELIVERED;
                            baseResponse.response.message.isRead = true;
                            storeMessageLocally(baseResponse.response.message, message.id, true);
                            storeGroupLocally(mGroupId, baseResponse.response.message);
                        }
                    });

            return;
        }

        mChatService.sendMessage(message.groupId, message.message)
                .compose(RxUtils.provideDefaultTransformer())
                .flatMap(new ErrorHandlingResponseConverter<>())
                .subscribe(new Subscriber<MessageResponse>() {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("resendMessage", "onError", e);
                        if(!isAttached()) {
                            return;
                        }
                        getView().enableMessageRetry(message.id);
                        if(mSystemUtils.isNetworkUnavailable()) {
                            getView().showError(R.string.error_no_internet_connection);
                            return;
                        }
                        getView().showError(R.string.error_retry_failed);
                    }

                    @Override
                    public void onNext(MessageResponse baseResponse) {
                        deleteLocalMessage(message);
                        baseResponse.response.message.status = Message.Status.DELIVERED;
                        storeMessageLocally(baseResponse.response.message, message.id, true);
                        storeGroupLocally(mGroupId, baseResponse.response.message);
                    }
                });
    }

    @Override
    public void sendMessage(String text) {
        Message message = new Message(findUniqueRandomId(mRealmProvider, mUserIdPreference.get()),
                mGroupId,
                System.currentTimeMillis() / 1000,
                text,
                null,
                null,
                null,
                null,
                mUserIdPreference.get(),
                mProfileNamePreference.get(),
                mAvatarPreference.get(),
                Message.Status.PENDING,
                true,
                mUserIdPreference.get(),
                false);

        storeMessageLocally(message)
                .subscribe(new Subscriber<Object>() {
                    @Override
                    public void onCompleted() {
                        if(mSystemUtils.isNetworkUnavailable()) {
                            getView().showError(R.string.error_no_internet_connection);
                            message.status = Message.Status.FAILED;
                            message.isRead = true;
                            storeMessageLocally(message, message.id, true);
                            storeGroupLocally(mGroupId, message);
                            return;
                        }

                        checkForEmptyMessageAndHideIt();
                        message.createdAt *= 1000;
                        getView().addOneMessage(message);
                        getView().scrollToPositionZero();

                        mChatService.sendMessage(mGroupId, text)
                                .compose(RxUtils.provideDefaultTransformer())
                                .flatMap(new ErrorHandlingResponseConverter<>())
                                .subscribe(new Subscriber<MessageResponse>() {
                                    @Override
                                    public void onCompleted() {
                                    }

                                    @Override
                                    public void onError(Throwable e) {
                                        Log.e("sendMessage", "onError", e);
                                        @SuppressWarnings("UnnecessaryLocalVariable") Message newMessage = message;
                                        newMessage.status = Message.Status.FAILED;
                                        newMessage.createdAt /= 1000;
                                        newMessage.isRead = true;
                                        storeMessageLocally(newMessage, message.id, false);
                                        storeGroupLocally(mGroupId, message);

                                        if(!isAttached()) {
                                            return;
                                        }

                                        if(mSystemUtils.isNetworkUnavailable()) {
                                            getView().showError(R.string.error_no_internet_connection);
                                            return;
                                        }

                                        getView().showError(R.string.error_something_went_wrong);
                                    }

                                    @Override
                                    public void onNext(MessageResponse baseResponse) {
                                        deleteLocalMessage(message);
                                        baseResponse.response.message.status = Message.Status.DELIVERED;
                                        baseResponse.response.message.isRead = true;
                                        storeMessageLocally(baseResponse.response.message, message.id, false);
                                        storeGroupLocally(mGroupId, baseResponse.response.message);
                                    }
                                });
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("sendMessage", "onError", e);
                    }

                    @Override
                    public void onNext(Object object) {
                    }
                });
    }

    private void storeMessageLocally(Message newMessage, String oldMessageId, boolean queryLocalDBWhenFinished) {
        newMessage.userId = mUserIdPreference.get();
        MessageByIdSpecification messageByIdSpecification = new MessageByIdSpecification(mUserIdPreference.get());
        messageByIdSpecification.setMessageId(newMessage.id);
        mMessageRealmRepository.update(Collections.singletonList(newMessage), messageByIdSpecification)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<Object>() {
                    @Override
                    public void onCompleted() {
                        newMessage.createdAt *= 1000;
                        if(!queryLocalDBWhenFinished) {
                            Message messageToDelete = new Message();
                            messageToDelete.id = oldMessageId;
                            messageToDelete.removeFromList(mMessages);
                            mMessages.add(newMessage);

                            if(!isAttached()) {
                                mMessageToUpdate.add(new Pair<>(newMessage, oldMessageId));
                                return;
                            }

                            getView().lastMessageUpdated(newMessage, oldMessageId);

                            return;
                        }

                        mPage = FIRST_PAGE;
                        mOffset = 0;
                        loadMessagesFromDBByGroupId();
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("addMessage", "onError", e);
                    }

                    @Override
                    public void onNext(Object o) {
                    }
                });
    }

    private void deleteLocalMessage(Message message) {
        MessageByIdSpecification messageByIdSpecification = new MessageByIdSpecification(mUserIdPreference.get());
        messageByIdSpecification.setMessageId(message.id);
        mMessageRealmRepository.remove(messageByIdSpecification)
                .subscribeOn(Schedulers.io())
                .subscribe(new Subscriber<Object>() {
                    @Override
                    public void onCompleted() {
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("deleteLocalMessage", "onError", e);
                    }

                    @Override
                    public void onNext(Object o) {

                    }
                });
    }

    @Override
    public void saveMessage(Message message) {
        storeMessageLocally(message)
                .subscribe(() -> {
                    if(!isAttached()) {
                        return;
                    }
                    checkForEmptyMessageAndHideIt();
                    message.createdAt *= 1000;
                    mMessages.add(message);
                    getView().addOneMessage(message);
                });
        storeGroupLocally(mGroupId, message);
    }

    private void checkForEmptyMessageAndHideIt() {
        if(mMessages.isEmpty()) {
            getView().hideLoading();
        }
    }

    private Completable storeMessageLocally(Message message) {
        MessageByIdSpecification messageByIdSpecification = new MessageByIdSpecification(mUserIdPreference.get());
        messageByIdSpecification.setMessageId(message.id);
        return mMessageRealmRepository.update(message, messageByIdSpecification)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * Save last message to local database.
     */
    private void storeGroupLocally(long groupId, Message message) {
        Group group = new Group();
        group.id = groupId;
        group.lastMessage = message;
        mGroupRealmRepository.updateGroupMessage(group, new GroupByIdSpecification(mUserIdPreference.get()))
                .subscribe(() -> {
                    if(!isAttached()) {
                        EventBus.getDefault().post(new GroupUpdatedEvent());
                    }
                });
    }

    @Override
    public void findGroupById() {
        Subscription subscription = mGroupRealmRepository.queryForOneById(String.valueOf(mGroupId), new GroupByIdSpecification(mUserIdPreference.get()))
                .compose(RxUtils.provideDefaultTransformer())
                .subscribe(new Subscriber<Group>() {
                    @Override
                    public void onCompleted() { }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(ChatPresenter.class.getSimpleName(), e.getMessage(), e);
                    }

                    @Override
                    public void onNext(Group group) {
                        if(group != null) {
                            getView().setGroup(group);
                            return;
                        }

                        loadGroups();
                    }
                });

        addSubscription(subscription);
    }

    private void loadGroups() {
        Subscription subscription = mChatService.getGroups()
                .compose(RxUtils.provideDefaultTransformer())
                .flatMap(new ErrorHandlingResponseConverter<>())
                .flatMap(chatResponse -> getGroupById(chatResponse.response.groups))
                .subscribe(this::groupsSuccess, this::groupsError);
        addSubscription(subscription);
    }

    private Observable<Group> getGroupById(ArrayList<Group> groups) {
        Group group = null;
        for(Group group1 : groups) {
            if(group1.id == mGroupId) {
                group = group1;
                break;
            }
        }
        return Observable.just(group);
    }

    private void groupsError(Throwable throwable) {
        Log.e(ChatPresenter.class.getSimpleName(), throwable.getMessage(), throwable);
        if(mSystemUtils.isNetworkUnavailable()) {
            getView().showError(R.string.error_no_internet_connection);
            return;
        }
        getView().showError(R.string.error_something_went_wrong);
    }

    private void groupsSuccess(Group group) {
        getView().setGroup(group);
        mGroupRealmRepository.update(group, new GroupByIdSpecification(mUserIdPreference.get())).subscribe();
    }

    @Override
    public long getGroupId() {
        return mGroupId;
    }

    @Override
    protected void afterAttachView(@NonNull ChatView view) {
        super.afterAttachView(view);
        view.setCallback(this);
        mFetchMore = true;
    }

    @Override
    protected void beforeDetachView(@NonNull ChatView view) {
        view.setCallback(null);
        super.beforeDetachView(view);
    }

    private Observable<String> downloadFileFromUrl(@NonNull String fileUrl, @NonNull String filePath, int itemPosition) {
        return Observable.create(subscriber -> {
            try {
                //set the download URL, a url that points to a file on the internet
                //this is the file to be downloaded
                URL url = new URL(fileUrl);
                //create the new connection
                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                //set up some things on the connection
                urlConnection.setRequestMethod("GET");
                urlConnection.setDoOutput(true);

                FileOutputStream fileOutput;
                InputStream inputStream;
                File file = new File(filePath);

                if(file.length() > 0) {
                    urlConnection.setRequestProperty("Range", "bytes=" + file.length() + "-");
                    //this will be used to append the downloaded data into the file already existing
                    fileOutput = new FileOutputStream(file, true);
                } else {
                    //this will be used to write the downloaded data into the file we created
                    fileOutput = new FileOutputStream(file);
                }

                //and connect!
                urlConnection.connect();

                //this is the total size of the file
                int totalSize = urlConnection.getContentLength();

                if(file.length() == totalSize || totalSize == -1) {
                    subscriber.onNext(FileUtils.getFileTypeFromFilePath(filePath));
                    subscriber.onCompleted();
                    return;
                }

                //this will be used in reading the data from the internet
                inputStream = urlConnection.getInputStream();

                //variable to store total downloaded bytes
                long downloadedSize = file.length();
                totalSize += downloadedSize;

                //create a buffer...
                byte[] buffer = new byte[1024];
                int bufferLength = 0;
                int lastPercentage = 0;
                int percentage = 0;

                //now, read through the input buffer and write the contents to the file
                while((bufferLength = inputStream.read(buffer, 0, 1024)) >= 0) {
                    //add the data in the buffer to the file in the file output stream (the file on the sd card
                    fileOutput.write(buffer, 0, bufferLength);
                    //add up the size so we know how much is downloaded
                    downloadedSize += bufferLength;
                    percentage = (int) (downloadedSize * 100 / totalSize);
                    if(percentage - lastPercentage > 1) {
                        lastPercentage = percentage;
                        if(isAttached()) {
                            getView().showItemProgress(itemPosition, percentage);
                        }
                    }
                }
                //close the output stream when done
                fileOutput.close();

                subscriber.onNext(FileUtils.getFileTypeFromFilePath(filePath));
                subscriber.onCompleted();

            } catch(IOException e) {
                e.printStackTrace();
                //noinspection ResultOfMethodCallIgnored
                subscriber.onError(e);
                subscriber.onCompleted();
            }
        });
    }

    public static String findUniqueRandomId(RealmProvider realmProvider, long userId) {
        String randomId = generateRandomId();
        MessageByIdSpecification messageByIdSpecification = new MessageByIdSpecification(userId);
        messageByIdSpecification.setMessageId(randomId);
        Realm realm = realmProvider.provide();
        realm.beginTransaction();
        boolean goOn = true;
        while(goOn) {
            MessageRealm messageRealm = messageByIdSpecification.toRealmQuery(realm).findFirst();
            if(messageRealm == null) {
                goOn = false;
            } else {
                randomId = generateRandomId();
                messageByIdSpecification.setMessageId(randomId);
            }
        }
        realm.commitTransaction();
        realm.close();
        return randomId;
    }

    private static String generateRandomId() {
        return "_" + new Random().nextInt();
    }

    public boolean hasNewerMessages(long groupId, String messageId) {
        return mGroupId == groupId && mMessages != null && mMessages.size() != 0 && !mMessages.get(0).id.equals(messageId);
    }
}