package com.viaccessorca.voplayerdemo.downloader;

import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.Toast;

import com.viaccessorca.exceptions.VOException;
import com.viaccessorca.vodownloader.VODownloader;
import com.viaccessorca.vodownloader.NanoHTTPD;
import com.viaccessorca.voplayer.VOPlayer;
import com.viaccessorca.voplayerdemo.Constants;
import com.viaccessorca.voplayerdemo.database.DatabaseInfo;
import com.viaccessorca.voplayerdemo.database.MediaMetaInfoDB;
import com.viaccessorca.voplayerdemo.downloader.DownloaderWrapper.OnErrorListener;
import com.viaccessorca.voplayerdemo.downloader.DownloaderWrapper.OnInfoListener;
import com.viaccessorca.voplayerdemo.downloader.DownloaderWrapper.OnPreparedListener;
import com.viaccessorca.voplayerdemo.downloader.DownloaderWrapper.State;
import com.viaccessorca.voplayerdemo.drm.DrmManager;


public class DownloadManager implements OnErrorListener, OnInfoListener, OnPreparedListener {

    private static final String TAG = "DownloadManager";

    /* These values are copied out of VODownloader (for API isolation purposes)
     They MUST stay in absolute sync with their originals*/
    public static final int DOWNLOADER_INFO_UNKNOWN = VODownloader.DOWNLOADER_INFO_UNKNOWN;
    public static final int DOWNLOADER_INFO_NETWORK_DOWN = VODownloader.DOWNLOADER_INFO_NETWORK_DOWN;
    public static final int DOWNLOADER_INFO_NETWORK_BACK = VODownloader.DOWNLOADER_INFO_NETWORK_BACK;
    public static final int DOWNLOADER_INFO_DOWNLOAD_PROGRESS = VODownloader.DOWNLOADER_INFO_DOWNLOAD_PROGRESS;
    public static final int DOWNLOADER_INFO_DOWNLOAD_COMPLETED = VODownloader.DOWNLOADER_INFO_DOWNLOAD_COMPLETED;
    public static final int DOWNLOADER_INFO_LOWSPACE = VODownloader.DOWNLOADER_INFO_LOWSPACE;
    public static final int DOWNLOADER_INFO_DOWNLOAD_SPEED = VODownloader.DOWNLOADER_INFO_DOWNLOAD_SPEED;

    /* These values are copied out of VODownloader (for API isolation purposes)
     They MUST stay in absolute sync with their originals*/
    public static final int DOWNLOADER_ERROR_UNKNOWN = VODownloader.DOWNLOADER_ERROR_UNKNOWN;
    public static final int DOWNLOADER_ERROR_NETWORK_BROKEN = VODownloader.DOWNLOADER_ERROR_NETWORK_BROKEN;
    public static final int DOWNLOADER_ERROR_NOMORESPACE = VODownloader.DOWNLOADER_ERROR_NOMORESPACE;
    public static final int DOWNLOADER_ERROR_PREPARED = VODownloader.DOWNLOADER_ERROR_VERSION_UNSUPPORTTED;
    public static final int DOWNLOADER_ERROR_OFFLINE_NOT_ALLOWED = VODownloader.DOWNLOADER_ERROR_OFFLINE_NOT_ALLOWED;
    public static final int DOWNLOADER_ERROR_LIVE_UNSUPPORTTED = VODownloader.DOWNLOADER_ERROR_LIVE_UNSUPPORTTED;

    private static final int MAX_NB_OF_DOWNLOADER = 3;

    private HashMap<String, DownloaderWrapper> downloaders = null;
    private DRMInfoHandler mDRMInfoHandler = null;
    private DrmManager.DRMActionPooler poolerExecutor = null;

    private NanoHTTPD nanoHTTPDServerRef = null;

    private Set<OnErrorListener> errorListeners = null;
    private Set<OnInfoListener> infoListeners = null;
    private Set<OnPreparedListener> prepareListeners = null;

    private Context context = null;

    // Factory management
    private static DownloadManager instance = null;

    public static DownloadManager getInstance(Context context) {
        if(null == instance) {
            instance = new DownloadManager(context);
        }
        return instance;
    }

    protected DownloadManager(Context ctx) {
        errorListeners = new HashSet<DownloaderWrapper.OnErrorListener>();
        infoListeners = new HashSet<DownloaderWrapper.OnInfoListener>();
        prepareListeners = new HashSet<DownloaderWrapper.OnPreparedListener>();

        this.context = ctx;

        downloaders = new HashMap<String, DownloaderWrapper>(0);
    }

    /**
     * Starts NanoHTTPD local server
     * @return true if the server has been correctly started, false otherwise
     */
    public boolean startLocalServer() {
        if (null == nanoHTTPDServerRef) {
            try {
                nanoHTTPDServerRef = new NanoHTTPD(Constants.LOCAL_HTTP_SERVER_PORT, new File(Constants.LOCAL_HTTP_SERVER_WWWROOT));
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
        else {
            return true;
        }
    }

    /**
     * Stops  NanoHTTPD local server (safe)
     */
    public void stopLocalServer() {
        if(null != nanoHTTPDServerRef) {
            try {
                nanoHTTPDServerRef.stop();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        nanoHTTPDServerRef = null;
    }

    /**
     * @return true if NanoHTTPD local server is running, false otherwise
     */
    public boolean isLocalServerRunning() {
        return (null != nanoHTTPDServerRef);
    }


    /**
     * Registers a listener to listen to all downloaders
     * @param listener
     */
    public void registerOnPreparedListener(OnPreparedListener listener) {
        if(! prepareListeners.contains(listener)) {
            prepareListeners.add(listener);
        }
    }

    /**
     * Registers a listener to listen to all downloaders
     * @param listener
     */
    public void unregisterOnPreparedListener(OnPreparedListener listener) {
        prepareListeners.remove(listener);
    }

    /**
     * Registers a listener to listen to all downloaders
     * @param listener
     */
    public void registerOnInfoListener(OnInfoListener listener) {
        infoListeners.add(listener);
    }

    public void unregisterOnInfoListener(OnInfoListener listener) {
        infoListeners.remove(listener);
    }

    public void registerOnErrorListener(OnErrorListener listener) {
        errorListeners.add(listener);
    }

    public void unregisterOnErrorListener(OnErrorListener listener) {
        errorListeners.remove(listener);
    }


    @Override
    public void onPrepared(DownloaderWrapper downloader) {
        Log.d(TAG, "onPrepared");
        for(OnPreparedListener listener: prepareListeners) {
            Log.d(TAG, "onPrepared >");
            listener.onPrepared(downloader);
        }
    }

    @Override
    public boolean onInfo(DownloaderWrapper downloader, int what, int extra) {
        Log.d(TAG, "onInfo " + what + " " + extra );

        boolean releaseDownloaderAfterNotifications = false;

        switch(what) {
        case VODownloader.DOWNLOADER_INFO_DOWNLOAD_COMPLETED:
            updateDlCompleted(downloader);
            releaseDownloaderAfterNotifications = true;
            break;

        case VODownloader.DOWNLOADER_INFO_DOWNLOAD_PROGRESS:
            updateDlProgress(downloader);
            break;
        }
        for(OnInfoListener listener: infoListeners) {
            listener.onInfo(downloader, what, extra);
        }

        if(releaseDownloaderAfterNotifications) {
            releaseDownloaderForMediaId(downloader.getMediaId());
        }
        return false;
    }


    private void updateDlProgress(DownloaderWrapper downloader) {

        String updatedMediaId = downloader.getMediaId();
        Log.d(TAG, "updateDlProgress - for mediaId: " + updatedMediaId);

        String localServerUrl = downloader.getLocalServerUrl();
        int downloadedDuration = downloader.getDownloadedDuration();

        MediaMetaInfoDB db = MediaMetaInfoDB.getInstance(context);
        DatabaseInfo dbInfo = db.getAssociatedDataForMedia(updatedMediaId);
        if (null == dbInfo) {
            dbInfo = new DatabaseInfo();
        }
        dbInfo.DlStatus = DatabaseInfo.DL_STATUS_DOWNLOADING;
        dbInfo.DLBitrate = downloader.getCurrentBitrate();
        dbInfo.DlLocalServerUrl = localServerUrl;
        dbInfo.DLProgress = downloadedDuration;
        dbInfo.mediaDurationInMs = downloader.getDuration();
        db.setDbInfoForMedia(updatedMediaId, dbInfo);
    }

    private void updateDlCompleted(DownloaderWrapper downloader) {

        String updatedMediaId = downloader.getMediaId();
        String localServerUrl = downloader.getLocalServerUrl();

        MediaMetaInfoDB db = MediaMetaInfoDB.getInstance(context);

        DatabaseInfo dbInfo = db.getAssociatedDataForMedia(updatedMediaId);
        if (null == dbInfo) {
            dbInfo = new DatabaseInfo();
        }
        dbInfo.DlStatus = DatabaseInfo.DL_STATUS_FULLY_LOCAL;
        dbInfo.DLProgress = 100;
        dbInfo.DlLocalServerUrl = localServerUrl;
        db.setDbInfoForMedia(updatedMediaId, dbInfo);
    }

    @Override
    public boolean onError(DownloaderWrapper gateway, int what, int extra) {
        Log.d(TAG, "onError " + what + " " + extra );
        for(OnErrorListener listener: errorListeners) {
            Log.d(TAG, "onError >");
            listener.onError(gateway, what, extra);
        }
        return false;
    }


    /**
     * @return number of instances the manager can still create
     */
    public int getNbAvailableDownloaders() {
        return MAX_NB_OF_DOWNLOADER - downloaders.size();
    }

    /**
     *
     * @param mediaId id of the stream managed by a downloader
     * @return the {@link DownloaderWrapper} instance in charge of handling the given Uri, or null if Uri is not handled
     */
    public DownloaderWrapper getDownloaderForMediaId(String mediaId) {

        return downloaders.get(mediaId);
    }

    /**
     * Creates and initialize an {@link DownloaderWrapper} to handle download of the given Uri.<br/>
     * @param mediaId media Id of the stream
     * @param streamUri Uri of the stream to use
     * @return an instance of {@link DownloaderWrapper} or null if there are no instances left available
     */
    public DownloaderWrapper createDownloaderForMediaId(String mediaId, String streamUri) {
        return createDownloaderForMediaId(mediaId, streamUri, null, null);
    }

    /**
     *  Creates and initialize an {@link DownloaderWrapper} to handle download of the given Uri with additional security parameters.<br/>
     * @param mediaId id of the stream
     * @param uri Uri of the stream to use
     * @param vmxBootAddress VMX boot address to use for the stream
     * @param vmxCompanyName VMX company name to use for the stream
     * @return an instance of {@link DownloaderWrapper} or null if there are no instances left available
     */
    public DownloaderWrapper createDownloaderForMediaId(String mediaId, String uri, String vmxBootAddress, String vmxCompanyName) {

        DownloaderWrapper downloader;

        downloader = getDownloaderForMediaId(mediaId);
        if(null != downloader) {
            return downloader;
        }
        else {
            if(downloaders.size() < MAX_NB_OF_DOWNLOADER) {
                downloader = new DownloaderWrapper(this.context, mediaId);

                downloader.init();

                downloader.setDownloadRootPath(Constants.LOCAL_HTTP_SERVER_WWWROOT);
                downloader.setLocalServerPort(Constants.LOCAL_HTTP_SERVER_PORT);

                downloader.setWrapperOnErrorListener(this);
                downloader.setWrapperOnInfoListener(this);
                downloader.setWrapperOnPreparedListener(this);
                downloader.setUri(uri);

                if (null != vmxBootAddress) {
                    if (null != downloader.getAgentVerimatrix()) {
                        downloader.getAgentVerimatrix().setBootAddress(vmxBootAddress);
                        downloader.getAgentVerimatrix().setCompanyName(vmxCompanyName);
                    }
                }

                // specifc code for PlayReady legacy configuration
                if (VOPlayer.getDRMAgentVersion() == VOPlayer.DRMAgentVersion.VERSION_1) {
                    try {
                        downloader.getAgentPlayready().setPersonalizationURL("file:///android_asset/PlayReady/");
                    } catch (VOException e) {
                        Log.e(TAG, "createDownloaderForUri exception while using playreadyAgent " + e.getStatus().toString());
                    }
                }

                downloaders.put(mediaId, downloader);
                return downloader;
            }
            else {
                Log.w(TAG, "createDownloaderForUri - too many downloaders already existing");
                return null;
            }
        }
    }

    /**
     * Frees an {@link DownloaderWrapper} instance of the manager.<br/>The stopping and destruction of the instance is
     * handled internally.
     * @param mediaId Uri of the stream to use
     */
    public boolean releaseDownloaderForMediaId(String mediaId) {

        DownloaderWrapper downloader = downloaders.remove(mediaId);

        if(null != downloader) {
            releaseDownloader(downloader);
            return true;
        }
        else {
            return false;
        }
    }


    public void releaseAllDownloaders() {
        Set<String> uris = downloaders.keySet();
        DownloaderWrapper downloader;
        for(String key : uris) {

            downloader = downloaders.remove(key);
            if(null != downloader) {
                releaseDownloader(downloader);
            }
        }
    }

    private boolean releaseDownloader(DownloaderWrapper downloader) {
        if(null != downloader) {
            State downloaderState = downloader.getState();
            if(State.DOWNLOADING == downloaderState) {
                MediaMetaInfoDB db = MediaMetaInfoDB.getInstance(context);

                String mediaId = downloader.getMediaId();
                DatabaseInfo dbInfo = db.getAssociatedDataForMedia(mediaId);
                if (null == dbInfo) {
                    dbInfo = new DatabaseInfo();
                }
                dbInfo.DlStatus = DatabaseInfo.DL_STATUS_PAUSED;
                dbInfo.DLBitrate = downloader.getCurrentBitrate();
                dbInfo.DlLocalServerUrl = downloader.getLocalServerUrl();
                dbInfo.DLProgress = downloader.getDownloadedDuration();
                db.setDbInfoForMedia(mediaId, dbInfo);

                try {
                    downloader.stop();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            if (poolerExecutor != null) {
                // Wait for all of executor to finish
                poolerExecutor.shutdown();

                try {
                    if (!poolerExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
                        // pool didn't terminate after the first try
                        poolerExecutor.shutdownNow();
                    }

                } catch (InterruptedException ex) {
                    poolerExecutor.shutdownNow();
                }
            }

            downloader.cleanup();
            return true;
        }
        else {
            return false;
        }
    }

    /**
     * Acquire rights if not present for selected uri and with the selected drm agent
     * @param params action parameters
     */
    public void acquireRightsIfNeededForUri(DrmManager.ActionParams params) {

        if (params.drmAgentType != null) {
            if (null == mDRMInfoHandler) {
                //create threads to handle DRM info
                mDRMInfoHandler = new DRMInfoHandler();
            }

            poolerExecutor = new DrmManager.DRMActionPooler(this.context, mDRMInfoHandler);
            poolerExecutor.submitDrmTask(params);
        }
    }

    private class DRMInfoHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            DrmManager.NotificationParams params = (DrmManager.NotificationParams)msg.obj;
            switch(params.notificationType){
                case DISPLAY_ERROR:
                    break;
                case ACQUIRE_RIGHTS_START:
                    Toast.makeText(context, "right acquisition start", Toast.LENGTH_SHORT).show();
                    break;
                case ACQUIRE_RIGHTS_DONE:
                    Toast.makeText(context, "right acquisition done", Toast.LENGTH_SHORT).show();
                    break;
                case DELETE_RIGHTS_START:
                    break;
                case DELETE_RIGHTS_DONE:
                    break;
                case PERSO_START:
                    break;
                case PERSO_DONE:
                    break;
                case DELETE_PERSO_START:
                    break;
                case DELETE_PERSO_DONE:
                    break;
                case GET_RIGHTS_INFO_START:
                    break;
                case GET_RIGHTS_INFO_DONE:
                    break;
                default:
                    break;
            }
        }
    };

}
