/*
 *  Copyright (C) 2004-2019 Savoir-faire Linux Inc.
 *
 *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
 *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package cx.ring.call;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

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

import cx.ring.facades.ConversationFacade;
import cx.ring.model.Conference;
import cx.ring.model.Conversation;
import cx.ring.model.SipCall;
import cx.ring.model.Uri;
import cx.ring.mvp.RootPresenter;
import cx.ring.services.AccountService;
import cx.ring.services.CallService;
import cx.ring.services.ContactService;
import cx.ring.services.DeviceRuntimeService;
import cx.ring.services.HardwareService;
import cx.ring.utils.Log;
import cx.ring.utils.StringUtils;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.Scheduler;
import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;

import static cx.ring.daemon.Ringservice.listCallMediaHandlers;
import static cx.ring.daemon.Ringservice.toggleCallMediaHandler;

public class CallPresenter extends RootPresenter<CallView> {

    public final static String TAG = CallPresenter.class.getSimpleName();

    private AccountService mAccountService;
    private ContactService mContactService;
    private HardwareService mHardwareService;
    private CallService mCallService;
    private DeviceRuntimeService mDeviceRuntimeService;
    private ConversationFacade mConversationFacade;

    private Conference mConference;
    private final List<SipCall> mPendingCalls = new ArrayList<>();
    private final Subject<List<SipCall>> mPendingSubject = BehaviorSubject.createDefault(mPendingCalls);

    private boolean mOnGoingCall = false;
    private boolean mAudioOnly = true;
    private boolean permissionChanged = false;
    private boolean pipIsActive = false;
    private boolean incomingIsFullIntent = true;
    private boolean callInitialized = false;

    private int videoWidth = -1;
    private int videoHeight = -1;
    private int previewWidth = -1;
    private int previewHeight = -1;
    private String currentSurfaceId = null;
    private String currentPluginSurfaceId = null;

    private Disposable timeUpdateTask = null;

    @Inject
    @Named("UiScheduler")
    protected Scheduler mUiScheduler;

    @Inject
    public CallPresenter(AccountService accountService,
                         ContactService contactService,
                         HardwareService hardwareService,
                         CallService callService,
                         DeviceRuntimeService deviceRuntimeService,
                         ConversationFacade conversationFacade) {
        mAccountService = accountService;
        mContactService = contactService;
        mHardwareService = hardwareService;
        mCallService = callService;
        mDeviceRuntimeService = deviceRuntimeService;
        mConversationFacade = conversationFacade;
    }

    public void cameraPermissionChanged(boolean isGranted) {
        if (isGranted && mHardwareService.isVideoAvailable()) {
            mHardwareService.initVideo().blockingAwait();
            permissionChanged = true;
        }
    }

    public void audioPermissionChanged(boolean isGranted) {
        if (isGranted && mHardwareService.hasMicrophone()) {
            mCallService.restartAudioLayer();
        }
    }


    @Override
    public void unbindView() {
        if (!mAudioOnly) {
            mHardwareService.endCapture();
        }
        super.unbindView();
    }

    @Override
    public void bindView(CallView view) {
        super.bindView(view);
        /*mCompositeDisposable.add(mAccountService.getRegisteredNames()
                .observeOn(mUiScheduler)
                .subscribe(r -> {
                    if (mSipCall != null && mSipCall.getContact() != null) {
                        getView().updateContactBubble(mSipCall.getContact());
                    }
                }));*/
        mCompositeDisposable.add(mHardwareService.getVideoEvents()
                .observeOn(mUiScheduler)
                .subscribe(this::onVideoEvent));
        mCompositeDisposable.add(mHardwareService.getAudioState()
                .observeOn(mUiScheduler)
                .subscribe(state -> getView().updateAudioState(state)));

        /*mCompositeDisposable.add(mHardwareService
                .getBluetoothEvents()
                .subscribe(event -> {
                    if (!event.connected && mSipCall == null) {
                        hangupCall();
                    }
                }));*/
    }

    public void initOutGoing(String accountId, String contactRingId, boolean audioOnly) {
        if (accountId == null || contactRingId == null) {
            Log.e(TAG, "initOutGoing: null account or contact");
            hangupCall();
            return;
        }
        if (!mHardwareService.hasCamera()) {
            audioOnly = true;
        }
        //getView().blockScreenRotation();

        mCompositeDisposable.add(mCallService
                .placeCall(accountId, StringUtils.toNumber(contactRingId), audioOnly)
                //.map(mCallService::getConference)
                .flatMapObservable(call -> mCallService.getConfUpdates(call))
                .observeOn(mUiScheduler)
                .subscribe(conference -> {
                    contactUpdate(conference);
                    confUpdate(conference);
                }, e -> {
                    hangupCall();
                    Log.e(TAG, "Error with initOutgoing: " + e.getMessage());
                }));
    }

    /**
     * Returns to or starts an incoming call
     *
     * @param confId         the call id
     * @param actionViewOnly true if only returning to call or if using full screen intent
     */
    public void initIncomingCall(String confId, boolean actionViewOnly) {
        //getView().blockScreenRotation();

        // if the call is incoming through a full intent, this allows the incoming call to display
        incomingIsFullIntent = actionViewOnly;

        Observable<Conference> callObservable = mCallService.getConfUpdates(confId)
                .observeOn(mUiScheduler)
                .share();

        // Handles the case where the call has been accepted, emits a single so as to only check for permissions and start the call once
        mCompositeDisposable.add(callObservable
                .firstOrError()
                .subscribe(call -> {
                    if (!actionViewOnly) {
                        contactUpdate(call);
                        confUpdate(call);
                        callInitialized = true;
                        getView().prepareCall(true);
                    }
                }, e -> {
                    hangupCall();
                    Log.e(TAG, "Error with initIncoming, preparing call flow :" , e);
                }));

        // Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call.
        mCompositeDisposable.add(callObservable
                .subscribe(call -> {
                    if (callInitialized || actionViewOnly) {
                        contactUpdate(call);
                        confUpdate(call);
                    }
                }, e -> {
                    hangupCall();
                    Log.e(TAG, "Error with initIncoming, action view flow: ", e);
                }));
    }

    public void prepareOptionMenu() {
        boolean isSpeakerOn = mHardwareService.isSpeakerPhoneOn();
        //boolean hasContact = mSipCall != null && null != mSipCall.getContact() && mSipCall.getContact().isUnknown();
        boolean canDial = mOnGoingCall && mConference != null && !mConference.isIncoming();
        // get the preferences
        boolean displayPluginsButton = getView().displayPluginsButton();
        boolean showPluginBtn = displayPluginsButton && mOnGoingCall && mConference != null;
        boolean hasMultipleCamera = mHardwareService.getCameraCount() > 1 && mOnGoingCall && !mAudioOnly;
        getView().initMenu(isSpeakerOn, hasMultipleCamera, canDial, showPluginBtn, mOnGoingCall);
    }

    public void chatClick() {
        if (mConference == null || mConference.getParticipants().isEmpty()) {
            return;
        }
        SipCall firstCall = mConference.getParticipants().get(0);
        if (firstCall == null
                || firstCall.getContact() == null
                || firstCall.getContact().getIds() == null
                || firstCall.getContact().getIds().isEmpty()) {
            return;
        }
        getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getIds().get(0));
    }

    public void speakerClick(boolean checked) {
        mHardwareService.toggleSpeakerphone(checked);
    }

    public void muteMicrophoneToggled(boolean checked) {
        mCallService.setMuted(checked);
    }


    public boolean isMicrophoneMuted() {
        return mCallService.isCaptureMuted();
    }

    public void switchVideoInputClick() {
        if(mConference == null)
            return;
        mHardwareService.switchInput(mConference.getId(), false);
        getView().switchCameraIcon(mHardwareService.isPreviewFromFrontCamera());
    }

    public void configurationChanged(int rotation) {
        mHardwareService.setDeviceOrientation(rotation);
    }

    public void dialpadClick() {
        getView().displayDialPadKeyboard();
    }

    public void acceptCall() {
        if (mConference == null) {
            return;
        }
        mCallService.accept(mConference.getId());
    }

    public void hangupCall() {
        List<String> callMediaHandlers = listCallMediaHandlers();

        for (String callMediaHandler : callMediaHandlers)
        {
            toggleCallMediaHandler(callMediaHandler, false);
        }

        if (mConference != null) {
            if (mConference.isConference())
                mCallService.hangUpConference(mConference.getId());
            else
                mCallService.hangUp(mConference.getId());
        }
        for (SipCall call : mPendingCalls) {
            mCallService.hangUp(call.getDaemonIdString());
        }
        finish();
    }

    public void refuseCall() {
        final Conference call = mConference;
        if (call != null) {
            mCallService.refuse(call.getId());
        }
        finish();
    }

    public void videoSurfaceCreated(Object holder) {
        if (mConference == null) {
            return;
        }
        String newId = mConference.getId();
        if (!newId.equals(currentSurfaceId)) {
            mHardwareService.removeVideoSurface(currentSurfaceId);
            currentSurfaceId = newId;
        }
        mHardwareService.addVideoSurface(mConference.getId(), holder);
        getView().displayContactBubble(false);
    }

    public void videoSurfaceUpdateId(String newId) {
        if (!Objects.equals(newId, currentSurfaceId)) {
            mHardwareService.updateVideoSurfaceId(currentSurfaceId, newId);
            currentSurfaceId = newId;
        }
    }

    public void pluginSurfaceCreated(Object holder) {
        if (mConference == null) {
            return;
        }
        String newId = mConference.getPluginId();
        if (!newId.equals(currentPluginSurfaceId)) {
            mHardwareService.removeVideoSurface(currentPluginSurfaceId);
            currentPluginSurfaceId = newId;
        }
        mHardwareService.addVideoSurface(mConference.getPluginId(), holder);
        getView().displayContactBubble(false);
    }

    public void pluginSurfaceUpdateId(String newId) {
        if (!Objects.equals(newId, currentPluginSurfaceId)) {
            mHardwareService.updateVideoSurfaceId(currentPluginSurfaceId, newId);
            currentPluginSurfaceId = newId;
        }
    }

    public void previewVideoSurfaceCreated(Object holder) {
        mHardwareService.addPreviewVideoSurface(holder, mConference);
        //mHardwareService.startCapture(null);
    }

    public void videoSurfaceDestroyed() {
        if (currentSurfaceId != null) {
            mHardwareService.removeVideoSurface(currentSurfaceId);
            currentSurfaceId = null;
        }
    }
    public void pluginSurfaceDestroyed() {
        if (currentPluginSurfaceId != null) {
            mHardwareService.removeVideoSurface(currentPluginSurfaceId);
            currentPluginSurfaceId = null;
        }
    }
    public void previewVideoSurfaceDestroyed() {
        mHardwareService.removePreviewVideoSurface();
        mHardwareService.endCapture();
    }

    public void displayChanged() {
        mHardwareService.switchInput(mConference.getId(), false);
    }

    public void layoutChanged() {
        //getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
    }


    public void uiVisibilityChanged(boolean displayed) {
        CallView view = getView();
        if (view != null)
            view.displayHangupButton(mOnGoingCall && displayed);
    }

    private void finish() {
        if (timeUpdateTask != null && !timeUpdateTask.isDisposed()) {
            timeUpdateTask.dispose();
            timeUpdateTask = null;
        }
        mConference = null;
        CallView view = getView();
        if (view != null)
            view.finish();
    }

    private Disposable contactDisposable = null;

    private void contactUpdate(final Conference conference) {
        if (mConference != conference) {
            mConference = conference;
            if (contactDisposable != null && !contactDisposable.isDisposed()) {
                contactDisposable.dispose();
            }
            if (conference.getParticipants().isEmpty())
                return;

            // Updates of participant (and  pending participant) list
            Observable<List<SipCall>> callsObservable = mPendingSubject
                    .map(pendingList -> {
                        Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size());
                        if (pendingList.isEmpty())
                            return conference.getParticipants();
                        List<SipCall> newList = new ArrayList<>(conference.getParticipants().size() + pendingList.size());
                        newList.addAll(conference.getParticipants());
                        newList.addAll(pendingList);
                        return newList;
                    });

            // Updates of individual contacts
            Observable<List<Observable<SipCall>>> contactsObservable = callsObservable
                    .flatMapSingle(calls -> Observable
                            .fromIterable(calls)
                            .map(call -> mContactService.observeContact(call.getAccount(), call.getContact())
                                    .map(contact -> call))
                            .toList(calls.size()));

            // Combined updates of contacts as participant list updates
            Observable<List<SipCall>> contactUpdates = contactsObservable
                    .switchMap(list -> Observable
                            .combineLatest(list, objects -> {
                                Log.w(TAG, "flatMapObservable " + objects.length);
                                ArrayList<SipCall> calls = new ArrayList<>(objects.length);
                                for (Object call : objects)
                                    calls.add((SipCall)call);
                                return (List<SipCall>)calls;
                            }))
                    .filter(list -> !list.isEmpty());

            contactDisposable = contactUpdates
                    .observeOn(mUiScheduler)
                    .subscribe(cs -> getView().updateContactBubble(cs), e -> Log.e(TAG, "Error updating contact data", e));
            mCompositeDisposable.add(contactDisposable);
        }
        mPendingSubject.onNext(mPendingCalls);
    }

    private void confUpdate(Conference call) {
        Log.w(TAG, "confUpdate " + call.getId());

        mConference = call;
        SipCall.CallStatus status = mConference.getState();
        if (status == SipCall.CallStatus.HOLD && mCallService.getConferenceList().size() == 1) {
            mCallService.unhold(mConference.getId());
        }
        mAudioOnly = !call.hasVideo();
        CallView view = getView();
        if (view == null)
            return;
        view.updateMenu();
        if (call.isOnGoing()) {
            mOnGoingCall = true;
            view.initNormalStateDisplay(mAudioOnly, isMicrophoneMuted());
            view.updateMenu();
            if (!mAudioOnly) {
                mHardwareService.setPreviewSettings();
                mHardwareService.updatePreviewVideoSurface(mConference);
                videoSurfaceUpdateId(call.getId());
                pluginSurfaceUpdateId(call.getPluginId());
                view.displayVideoSurface(true, mDeviceRuntimeService.hasVideoPermission());
                if (permissionChanged) {
                    mHardwareService.switchInput(mConference.getId(), permissionChanged);
                    permissionChanged = false;
                }
            }
            if (timeUpdateTask != null)
                timeUpdateTask.dispose();
            timeUpdateTask = mUiScheduler.schedulePeriodicallyDirect(this::updateTime, 0, 1, TimeUnit.SECONDS);
        } else if (call.isRinging()) {
            SipCall scall = call.getCall();

            view.handleCallWakelock(mAudioOnly);
            if (scall.isIncoming()) {
                if (mAccountService.getAccount(scall.getAccount()).isAutoanswerEnabled()) {
                    mCallService.accept(scall.getDaemonIdString());
                    // only display the incoming call screen if the notification is a full screen intent
                } else if (incomingIsFullIntent) {
                    view.initIncomingCallDisplay();
                }
            } else {
                mOnGoingCall = false;
                view.updateCallStatus(scall.getCallStatus());
                view.initOutGoingCallDisplay();
            }
        } else {
            finish();
        }
    }

    private void updateTime() {
        CallView view = getView();
        if (view != null && mConference != null) {
            if (mConference.isOnGoing()) {
                long start = mConference.getTimestampStart();
                if (start != Long.MAX_VALUE) {
                    view.updateTime((System.currentTimeMillis() - start) / 1000);
                } else {
                    view.updateTime(-1);
                }
            }
        }
    }

    private void onVideoEvent(HardwareService.VideoEvent event) {
        Log.d(TAG, "VIDEO_EVENT: " + event.start + " " + event.callId + " " + event.w + "x" + event.h);

        if (event.start) {
            getView().displayVideoSurface(true, !isPipMode() && mDeviceRuntimeService.hasVideoPermission());
        } else if (mConference != null && mConference.getId().equals(event.callId)) {
            getView().displayVideoSurface(event.started, event.started && !isPipMode() && mDeviceRuntimeService.hasVideoPermission());
            if (event.started) {
                videoWidth = event.w;
                videoHeight = event.h;
                getView().resetVideoSize(videoWidth, videoHeight);
            }
        } else if (event.callId == null) {
            if (event.started) {
                previewWidth = event.w;
                previewHeight = event.h;
                getView().resetPreviewVideoSize(previewWidth, previewHeight, event.rot);
            }
        }
        if (mConference != null && mConference.getPluginId().equals(event.callId)) {
            if (event.started) {
                previewWidth = event.w;
                previewHeight = event.h;
                getView().resetPluginPreviewVideoSize(previewWidth, previewHeight, event.rot);
            }
        }
        /*if (event.started || event.start) {
            getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
        }*/
    }

    public void positiveButtonClicked() {
        if (mConference.isRinging() && mConference.isIncoming()) {
            acceptCall();
        } else {
            hangupCall();
        }
    }

    public void negativeButtonClicked() {
        if (mConference.isRinging() && mConference.isIncoming()) {
            refuseCall();
        } else {
            hangupCall();
        }
    }

    public void toggleButtonClicked() {
        if (mConference != null && !(mConference.isRinging() && mConference.isIncoming())) {
            hangupCall();
        }
    }

    public boolean isAudioOnly() {
        return mAudioOnly;
    }

    public void requestPipMode() {
        if (mConference != null && mConference.isOnGoing() && mConference.hasVideo()) {
            getView().enterPipMode(mConference.getId());
        }
    }

    public boolean isPipMode() {
        return pipIsActive;
    }

    public void pipModeChanged(boolean pip) {
        pipIsActive = pip;
        if (pip) {
            getView().displayHangupButton(false);
            getView().displayPreviewSurface(false);
            getView().displayVideoSurface(true, false);
        } else {
            getView().displayPreviewSurface(true);
            getView().displayVideoSurface(true, mDeviceRuntimeService.hasVideoPermission());
        }
    }

    public boolean isSpeakerphoneOn() {
        return mHardwareService.isSpeakerPhoneOn();
    }

    public void sendDtmf(CharSequence s) {
        mCallService.playDtmf(s.toString());
    }

    public void addConferenceParticipant(String accountId, String contactId) {
        mCompositeDisposable.add(mConversationFacade.startConversation(accountId, new Uri(contactId))
                .map(Conversation::getCurrentCalls)
                .subscribe(confs -> {
                    if (confs.isEmpty()) {
                        final Observer<SipCall> pendingObserver = new Observer<SipCall>() {
                            private SipCall call = null;
                            @Override
                            public void onSubscribe(Disposable d) {}

                            @Override
                            public void onNext(SipCall sipCall) {
                                if (call == null) {
                                    call = sipCall;
                                    mPendingCalls.add(sipCall);
                                }
                                mPendingSubject.onNext(mPendingCalls);
                            }

                            @Override
                            public void onError(Throwable e) {}

                            @Override
                            public void onComplete() {
                                if (call != null) {
                                    mPendingCalls.remove(call);
                                    mPendingSubject.onNext(mPendingCalls);
                                    call = null;
                                }
                            }
                        };

                        // Place new call, join to conference when answered
                        Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, contactId, mAudioOnly)
                                .doOnEach(pendingObserver)
                                .filter(SipCall::isOnGoing)
                                .firstElement()
                                .delay(1, TimeUnit.SECONDS)
                                .doOnEvent((v, e) -> pendingObserver.onComplete());
                        mCompositeDisposable.add(newCall.subscribe(call ->  {
                            String id = mConference.getId();
                            if (mConference.isConference()) {
                                mCallService.addParticipant(call.getDaemonIdString(), id);
                            } else {
                                mCallService.joinParticipant(id, call.getDaemonIdString()).subscribe();
                            }
                        }));
                    } else {
                        // Selected contact already in call or conference, join it to current conference
                        Conference selectedConf = confs.get(0);
                        if (selectedConf != mConference) {
                            if (mConference.isConference()) {
                                if (selectedConf.isConference())
                                    mCallService.joinConference(mConference.getId(), selectedConf.getId());
                                else
                                    mCallService.addParticipant(selectedConf.getId(), mConference.getId());
                            } else {
                                if (selectedConf.isConference())
                                    mCallService.addParticipant(mConference.getId(), selectedConf.getId());
                                else
                                    mCallService.joinParticipant(mConference.getId(), selectedConf.getId()).subscribe();
                            }
                        }
                    }
                }));
    }

    public void startAddParticipant() {
        getView().startAddParticipant(mConference.getId());
    }

    public void hangupParticipant(SipCall call) {
        mCallService.hangUp(call.getDaemonIdString());
    }

    public void openParticipantContact(SipCall call) {
        getView().goToContact(call.getAccount(), call.getContact());
    }

    public void stopCapture() {
        mHardwareService.stopCapture();
    }

    public boolean startScreenShare(Object mediaProjection) {
        return mHardwareService.startScreenShare(mediaProjection);
    }

    public void stopScreenShare() {
        mHardwareService.stopScreenShare();
    }
}