package jp.manse;

import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.brightcove.player.edge.Catalog;
import com.brightcove.player.edge.OfflineCallback;
import com.brightcove.player.edge.OfflineCatalog;
import com.brightcove.player.edge.PlaylistListener;
import com.brightcove.player.model.Playlist;
import com.brightcove.player.model.Video;
import com.brightcove.player.network.DownloadStatus;
import com.facebook.react.bridge.NativeArray;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jp.manse.util.DefaultEventEmitter;

public class BrightcovePlayerAccount implements OfflineVideoDownloadSession.OnOfflineVideoDownloadSessionListener {
    final private static int FPS = 40;
    final private static String DEBUG_TAG = "brightcoveplayer";
    final private static String ERROR_CODE = "error";
    final private static String ERROR_MESSAGE_DUPLICATE_SESSION = "Offline video or download session already exists";
    final private static String ERROR_MESSAGE_DELETE = "Could not delete video";
    final private static String ERROR_MESSAGE_PLAYLIST = "Failed to load playlist";
    final private static String CALLBACK_KEY_VIDEO_TOKEN = "videoToken";
    final private static String CALLBACK_KEY_DOWNLOAD_PROGRESS = "downloadProgress";
    final private static String CALLBACK_KEY_ACCOUNT_ID = "accountId";
    final private static String CALLBACK_KEY_VIDEO_ID = "videoId";
    final private static String CALLBACK_KEY_REFERENCE_ID = "referenceId";
    final private static String CALLBACK_KEY_NAME = "name";
    final private static String CALLBACK_KEY_DESCRIPTION = "description";
    final private static String CALLBACK_KEY_DURATION = "duration";

    private ReactApplicationContext context;
    public String accountId;
    public String policyKey;

    private Handler handler;
    private List<OfflineVideoDownloadSession> offlineVideoDownloadSessions = new ArrayList<>();
    private boolean getOfflineVideoStatusesRunning = false;
    private List<Promise> getOfflineVideoStatusesPendingPromises = new ArrayList<>();
    private List<Video> allDownloadedVideos = new ArrayList<>();
    private Catalog catalog;
    private OfflineCatalog offlineCatalog;
    private OnBrightcovePlayerAccountListener listener;

    public interface OnBrightcovePlayerAccountListener {
        void onOfflineStorageStateChanged(NativeArray array);
    }

    public BrightcovePlayerAccount(final ReactApplicationContext context, final String accountId, final String policyKey, OnBrightcovePlayerAccountListener listener) {
        this.context = context;
        this.accountId = accountId;
        this.policyKey = policyKey;
        this.listener = listener;
        handler = new Handler(Looper.myLooper());
        this.catalog = new Catalog(DefaultEventEmitter.sharedEventEmitter, accountId, policyKey);
        this.offlineCatalog = new OfflineCatalog(context, DefaultEventEmitter.sharedEventEmitter, accountId, policyKey);
        this.offlineCatalog.setMeteredDownloadAllowed(true);
        this.offlineCatalog.setMobileDownloadAllowed(true);
        this.offlineCatalog.setRoamingDownloadAllowed(true);
        this.offlineCatalog.findAllVideoDownload(DownloadStatus.STATUS_DOWNLOADING, new OfflineCallback<List<Video>>() {
            @Override
            public void onSuccess(List<Video> videos) {
                for (Video video : videos) {
                    OfflineVideoDownloadSession session = new OfflineVideoDownloadSession(context, accountId, policyKey, BrightcovePlayerAccount.this);
                    session.resumeDownload(video);
                    offlineVideoDownloadSessions.add(session);
                }
            }

            @Override
            public void onFailure(Throwable throwable) {
            }
        });
    }

    public void requestDownloadWithReferenceId(String referenceId, int bitRate, Promise promise) {
        if (this.hasOfflineVideoDownloadSessionWithReferenceId(referenceId)) {
            promise.reject(ERROR_CODE, ERROR_MESSAGE_DUPLICATE_SESSION);
            return;
        }
        OfflineVideoDownloadSession session = new OfflineVideoDownloadSession(this.context, this.accountId, this.policyKey, this);
        session.requestDownloadWithReferenceId(referenceId, bitRate, promise);
        this.offlineVideoDownloadSessions.add(session);
        this.listener.onOfflineStorageStateChanged(collectNativeOfflineVideoStatuses());
    }

    public void requestDownloadWithVideoId(String videoId, int bitRate, Promise promise) {
        if (this.hasOfflineVideoDownloadSessionWithVideoId(videoId)) {
            promise.reject(ERROR_CODE, ERROR_MESSAGE_DUPLICATE_SESSION);
            return;
        }
        OfflineVideoDownloadSession session = new OfflineVideoDownloadSession(this.context, this.accountId, this.policyKey, this);
        session.requestDownloadWithVideoId(videoId, bitRate, promise);
        this.offlineVideoDownloadSessions.add(session);
        this.listener.onOfflineStorageStateChanged(collectNativeOfflineVideoStatuses());
    }

    public void getOfflineVideoStatuses(Promise promise) {
        if (this.getOfflineVideoStatusesRunning) {
            this.getOfflineVideoStatusesPendingPromises.add(promise);
            return;
        }
        this.getOfflineVideoStatusesRunning = true;
        this.getOfflineVideoStatusesPendingPromises.clear();
        this.getOfflineVideoStatusesPendingPromises.add(promise);
        final Runnable updateRunnable = new Runnable() {
            @Override
            public void run() {
                sendOfflineVideoStatuses();
                getOfflineVideoStatusesRunning = false;
            }
        };
        this.offlineCatalog.findAllVideoDownload(DownloadStatus.STATUS_COMPLETE, new OfflineCallback<List<Video>>() {
            @Override
            public void onSuccess(List<Video> videos) {
                allDownloadedVideos = videos;
                handler.postDelayed(updateRunnable, FPS);
            }

            @Override
            public void onFailure(Throwable throwable) {
                for (Promise promise : getOfflineVideoStatusesPendingPromises) {
                    promise.reject(ERROR_CODE, ERROR_CODE);
                }
                getOfflineVideoStatusesRunning = false;
            }
        });
    }

    private void sendOfflineVideoStatuses() {
        for (Promise promise : getOfflineVideoStatusesPendingPromises) {
            promise.resolve(this.collectNativeOfflineVideoStatuses());
        }
    }

    public void deleteOfflineVideo(final String videoId, final Promise promise) {
        try {
            if (videoId == null) throw new Exception();
            this.offlineCatalog.cancelVideoDownload(videoId);
            for (int i = this.offlineVideoDownloadSessions.size() - 1; i >= 0; i--) {
                OfflineVideoDownloadSession session = this.offlineVideoDownloadSessions.get(i);
                if (videoId.equals(session.videoId)) {
                    this.offlineVideoDownloadSessions.remove(i);
                }
            }
            this.offlineCatalog.deleteVideo(videoId, new OfflineCallback<Boolean>() {
                @Override
                public void onSuccess(Boolean aBoolean) {
                    promise.resolve(null);
                    for (int i = allDownloadedVideos.size() - 1; i >= 0; i--) {
                        Video video = allDownloadedVideos.get(i);
                        if (videoId.equals(video.getId())) {
                            allDownloadedVideos.remove(i);
                        }
                    }
                    listener.onOfflineStorageStateChanged(collectNativeOfflineVideoStatuses());
                }

                @Override
                public void onFailure(Throwable throwable) {
                    promise.reject(ERROR_CODE, ERROR_MESSAGE_DELETE);
                }
            });
        } catch (Exception e) {
            Log.e(DEBUG_TAG, e.getMessage());
            promise.reject(ERROR_CODE, e);
        }
    }

    public void getPlaylistWithPlaylistId(String playlistId, final Promise promise) {
        catalog.findPlaylistByID(playlistId, new PlaylistListener() {
            @Override
            public void onPlaylist(Playlist playlist) {
                promise.resolve(collectNativePlaylist(accountId, playlist));
            }

            @Override
            public void onError(String error) {
                promise.reject(ERROR_CODE, ERROR_MESSAGE_PLAYLIST);
            }
        });
    }

    public void getPlaylistWithReferenceId(String referenceId, final Promise promise) {
        catalog.findPlaylistByReferenceID(referenceId, new PlaylistListener() {
            @Override
            public void onPlaylist(Playlist playlist) {
                promise.resolve(collectNativePlaylist(accountId, playlist));
            }

            @Override
            public void onError(String error) {
                promise.reject(ERROR_CODE, ERROR_MESSAGE_PLAYLIST);
            }
        });
    }

    private NativeArray collectNativeOfflineVideoStatuses() {
        WritableNativeArray statuses = new WritableNativeArray();
        for (Video video : this.allDownloadedVideos) {
            WritableNativeMap map = new WritableNativeMap();
            map.putString(CALLBACK_KEY_ACCOUNT_ID, this.accountId);
            map.putString(CALLBACK_KEY_VIDEO_ID, video.getId());
            map.putString(CALLBACK_KEY_VIDEO_TOKEN, video.getId());
            map.putDouble(CALLBACK_KEY_DOWNLOAD_PROGRESS, 1);
            statuses.pushMap(map);
        }
        for (OfflineVideoDownloadSession session : this.offlineVideoDownloadSessions) {
            if (session.videoId == null) continue;
            boolean found = false;
            for (Video video : this.allDownloadedVideos) {
                if (session.videoId.equals(video.getId())) {
                    found = true;
                    break;
                }
            }
            if (found) continue;
            WritableNativeMap map = new WritableNativeMap();
            map.putString(CALLBACK_KEY_ACCOUNT_ID, this.accountId);
            map.putString(CALLBACK_KEY_VIDEO_ID, session.videoId);
            map.putString(CALLBACK_KEY_VIDEO_TOKEN, session.videoId);
            map.putDouble(CALLBACK_KEY_DOWNLOAD_PROGRESS, session.downloadProgress);
            statuses.pushMap(map);
        }
        return statuses;
    }

    private NativeArray collectNativePlaylist(String accountId, Playlist playlist) {
        WritableNativeArray result = new WritableNativeArray();
        for (Video video : playlist.getVideos()) {
            WritableNativeMap map = new WritableNativeMap();
            map.putString(CALLBACK_KEY_ACCOUNT_ID, accountId);
            map.putString(CALLBACK_KEY_VIDEO_ID, video.getId());
            map.putString(CALLBACK_KEY_REFERENCE_ID, video.getReferenceId());
            map.putString(CALLBACK_KEY_NAME, video.getName());
            map.putString(CALLBACK_KEY_DESCRIPTION, video.getDescription());
            map.putInt(CALLBACK_KEY_DURATION, video.getDuration());
            result.pushMap(map);
        }
        return result;
    }

    private boolean hasOfflineVideoDownloadSessionWithReferenceId(String referenceId) {
        for (OfflineVideoDownloadSession session : this.offlineVideoDownloadSessions) {
            if (referenceId.equals(session.referenceId)) return true;
        }
        return false;
    }

    private boolean hasOfflineVideoDownloadSessionWithVideoId(String videoId) {
        for (OfflineVideoDownloadSession session : this.offlineVideoDownloadSessions) {
            if (videoId.equals(session.videoId)) return true;
        }
        return false;
    }

    @Override
    public void onCompleted(OfflineVideoDownloadSession session) {
        this.offlineVideoDownloadSessions.remove(session);
        if (session.downloadProgress == 1) {
            Map<String, Object> param = new HashMap<>();
            param.put(Video.Fields.ID, session.videoId);
            this.allDownloadedVideos.add(new Video(param));
        }
        this.listener.onOfflineStorageStateChanged(collectNativeOfflineVideoStatuses());
    }

    @Override
    public void onProgress() {
        this.listener.onOfflineStorageStateChanged(collectNativeOfflineVideoStatuses());
    }
}