/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ohos.ace.plugin.playerplugin;

import android.content.Context;
import android.media.MediaDataSource;
import android.media.MediaFormat;
import android.media.MediaPlayer;
import android.media.PlaybackParams;
import android.os.Handler;
import android.view.Surface;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.Runnable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import ohos.ace.adapter.ALog;

/**
 * PlayerPlugin
 *
 * @since 1
 */
public class PlayerPlugin {
    /**
     * player instance
     */
    private static final String LOG_TAG = "PlayerPlugin";

    /**
     * player info type speed done
     */
    public static final int PLAYER_INFO_TYPE_SPEEDDONE = 2;

    /**
     * player info type bitrate done
     */
    public static final int PLAYER_INFO_TYPE_BITRATEDONE = 3;

    /**
     * player info type eos
     */
    public static final int PLAYER_INFO_TYPE_EOS = 4;

    /**
     * player info type state change
     */
    public static final int PLAYER_INFO_TYPE_STATE_CHANGE = 5;

    /**
     * player info type position update
     */
    public static final int PLAYER_INFO_TYPE_POSITION_UPDATE = 6;

    /**
     * player info type duration update
     */
    public static final int PLAYER_INFO_TYPE_DURATION_UPDATE = 15;

    /**
     * PLAYER_STATE_ERROR
     */
    public static final int PLAYER_STATE_ERROR = 0;

    /**
     * PLAYER_STATE_IDLE
     */
    public static final int PLAYER_STATE_IDLE = 1;

    /**
     * PLAYER_STATE_INITIALIZED
     */
    public static final int PLAYER_STATE_INITIALIZED = 2;

    /**
     * PLAYER_STATE_PREPARING
     */
    public static final int PLAYER_STATE_PREPARED = 4;

    /**
     * PLAYER_STATE_STARTED
     */
    public static final int PLAYER_STATE_STARTED = 5;

    /**
     * PLAYER_STATE_PAUSED
     */
    public static final int PLAYER_STATE_PAUSED = 6;

    /**
     * PLAYER_STATE_STOPPED
     */
    public static final int PLAYER_STATE_STOPPED = 7;

    /**
     * PLAYER_STATE_PLAYBACK_COMPLETE
     */
    public static final int PLAYER_STATE_PLAYBACK_COMPLETE = 8;

    /**
     * PLAYER_STATE_RELEASED
     */
    public static final int PLAYER_STATE_RELEASED = 9;

    /**
     * PLAYER_TIME_UPDATE_TIME_DELAY
     */
    public static final int PLAYER_TIME_UPDATE_TIME_DELAY = 100;

    /**
     * PLAYER_TRACK_CODEC_MIME
     */
    private static final String PLAYER_TRACK_CODEC_MIME = "codec_mime";

    /**
     * PLAYER_CODEC_MIME_LENGTH
     */
    private static final int PLAYER_CODEC_MIME_LENGTH = 5;

    /**
     * player instance
     */
    private volatile Map<Long, MediaPlayer> mediaPlayerMap;

    /**
     * media data source
     */
    private Map<Long, MediaDataSourceImpl> mediaDataSourceMap;

    /**
     * PlayerPlugin
     *
     * @param context context of the application
     */
    public PlayerPlugin(Context context) {
        if (context == null) {
            ALog.e(LOG_TAG, "context is null");
            return;
        }
        mediaPlayerMap = new HashMap<Long, MediaPlayer>();
        mediaDataSourceMap = new HashMap<Long, MediaDataSourceImpl>();
        runTimeUpdate();
        nativeInit();
    }

    /**
     * PreparedCallbackImpl implementation
     */
    public class PreparedCallbackImpl implements MediaPlayer.OnPreparedListener {
        @Override
        public void onPrepared(MediaPlayer mp) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                notifyInfo(key, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_PREPARED);
                notifyInfo(key, PLAYER_INFO_TYPE_DURATION_UPDATE, mp.getDuration());
                notifyInfo(key, PLAYER_INFO_TYPE_POSITION_UPDATE, 0);
            }
        }
    }

    /**
     * CompletionCallbackImpl
     */
    public class CompletionCallbackImpl implements MediaPlayer.OnCompletionListener {
        @Override
        public void onCompletion(MediaPlayer mp) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                nativeOnInfo(key, PLAYER_INFO_TYPE_EOS, 0);
                nativeOnInfo(key, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_PLAYBACK_COMPLETE);
                nativeOnInfo(key, PLAYER_INFO_TYPE_POSITION_UPDATE, mp.getDuration());
            }
        }
    }

    /**
     * SeekCompletionCallbackImpl
     */
    public class SeekCompletionCallbackImpl implements MediaPlayer.OnSeekCompleteListener {
        @Override
        public void onSeekComplete(MediaPlayer mp) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                nativeOnSeekComplete(key, mp.getCurrentPosition());
                notifyInfo(key, PLAYER_INFO_TYPE_POSITION_UPDATE, mp.getCurrentPosition());
            }
        }
    }

    /**
     * BufferingUpdateCallbackImpl
     */
    public class BufferingUpdateCallbackImpl implements MediaPlayer.OnBufferingUpdateListener {
        @Override
        public void onBufferingUpdate(MediaPlayer mp, int percent) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                nativeOnBufferingUpdate(key, percent);
            }
        }
    }

    /**
     * VideoSizeChangedCallbackImpl
     */
    public class VideoSizeChangedCallbackImpl implements MediaPlayer.OnVideoSizeChangedListener {
        @Override
        public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                nativeOnVideoSizeChanged(key, width, height);
            }
        }
    }

    /**
     * ErrorCallbackImpl
     */
    public class ErrorCallbackImpl implements MediaPlayer.OnErrorListener {
        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            long key = getMediaPlayerKey(mp);
            if (key != 0) {
                nativeOnError(key,
                    what == MediaPlayer.MEDIA_ERROR_UNKNOWN || what == MediaPlayer.MEDIA_ERROR_SERVER_DIED ?
                    what : extra);
                return true;
            }
            return false;
        }
    }

    /**
     * MediaDataSourceImpl
     *
     * The class is used for getting the data source.
     * @since 1
     */
    public class MediaDataSourceImpl extends MediaDataSource {
        private long key;

        public MediaDataSourceImpl(long id) {
            key = id;
        }

        @Override
        public long getSize() {
            return -1;
        }

        @Override
        public int readAt(long position, byte[] buffer, int offset, int size) {
            return nativeReadAt(key, position, buffer, offset, size);
        }

        @Override
        public void close() {
        }
    };

    /**
     * CallbackThread
     */
    private class CallbackThread extends Thread {
        private long id;
        private int infoType;
        private int info;

        @Override
        public void run() {
            nativeOnInfo(id, infoType, info);
        }

        public void setParams(long key, int what, int extra) {
            id = key;
            infoType = what;
            info = extra;
        }
    }

    /**
     * addCallbackThread to callback thread pool
     *
     * @param id the player id
     */
    public void addMediaPlayer(long id) {
        MediaPlayer mp = mediaPlayerMap.get(id);
        if (mp != null) {
            ALog.e(LOG_TAG, "addMediaPlayer mediaPlayer exist.");
            return;
        }
        mp = new MediaPlayer();
        mediaPlayerMap.put(id, mp);
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_IDLE);

        mp.setOnPreparedListener(new PlayerPlugin.PreparedCallbackImpl());
        mp.setOnErrorListener(new PlayerPlugin.ErrorCallbackImpl());
        mp.setOnCompletionListener(new PlayerPlugin.CompletionCallbackImpl());
        mp.setOnSeekCompleteListener(new PlayerPlugin.SeekCompletionCallbackImpl());
        mp.setOnBufferingUpdateListener(new PlayerPlugin.BufferingUpdateCallbackImpl());
        mp.setOnVideoSizeChangedListener(new PlayerPlugin.VideoSizeChangedCallbackImpl());
    }

    private MediaPlayer getMediaPlayerById(long id) {
        return mediaPlayerMap.get(id);
    }

    private long getMediaPlayerKey(MediaPlayer mp) {
        for (long key : mediaPlayerMap.keySet()) {
            MediaPlayer value = mediaPlayerMap.get(key);
            if (value == mp) {
                return key;
            }
        }
        return 0;
    }

    private void notifyInfo(long key, int what, int extra) {
        CallbackThread td = new CallbackThread();
        td.setParams(key, what, extra);
        td.start();
    }

    private void runTimeUpdate() {
        Handler handler = new Handler();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (long key : mediaPlayerMap.keySet()) {
                    MediaPlayer mp = mediaPlayerMap.get(key);
                    if (mp.isPlaying()) {
                        notifyInfo(key, PLAYER_INFO_TYPE_POSITION_UPDATE, mp.getCurrentPosition());
                    }
                }
                handler.postDelayed(this, PLAYER_TIME_UPDATE_TIME_DELAY);
            }
        };
        handler.postDelayed(runnable, 0);
    }

    /**
     * releaseMediaPlayer release the MediaPlayer
     *
     * @param key the key of MediaPlayer
     */
    public void releaseMediaPlayer(long key) {
        mediaPlayerMap.remove(key);
    }

    /**
     * setDataSource set the MediaPlayer data source with MediaDataSource
     *
     * @param id the key of MediaPlayer
     */
    public void setDataSource(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }

        MediaDataSourceImpl dataSource = new MediaDataSourceImpl(id);
        mediaDataSourceMap.put(id, dataSource);

        mp.setDataSource(dataSource);
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_INITIALIZED);
    }

    /**
     * setDataSourceWithUrl set the MediaPlayer data source with url
     *
     * @param id  the key of MediaPlayer
     * @param url  the file path
     */
    public void setDataSourceWithUrl(long id, String url) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        try {
            mp.setDataSource(url);
            notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_INITIALIZED);
        } catch (IOException ex) {
            ALog.e(LOG_TAG, "setDataSourceWithUrl IOException:" + ex.getMessage());
        }
    }

    /**
     * setDataSourceWithFd set the MediaPlayer data source with file descriptor
     *
     * @param id    the key of MediaPlayer
     * @param url   the file path
     * @param offset the offset of the file
     * @param length the length of the file
     */
    public void setDataSourceWithFd(long id, String url, long offset, long length) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        try {
            if (length <= 0) {
                File file = new File(url);
                length = file.length();
            }
            FileInputStream fs = new FileInputStream(url);

            mp.setDataSource(fs.getFD(), offset, length);
            notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_INITIALIZED);
        } catch (IOException ex) {
            ALog.e(LOG_TAG, "setDataSourceWithFd IOException:" + ex.getMessage());
        }
    }

    /**
     * prepare prepare the MediaPlayer
     *
     * @param id the key of MediaPlayer
     */
    public void prepare(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        try {
            mp.prepare();
            notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_PREPARED);
            notifyInfo(id, PLAYER_INFO_TYPE_DURATION_UPDATE, mp.getDuration());
        } catch (IOException ex) {
            ALog.e(LOG_TAG, "prepare IOException:" + ex.getMessage());
        }
    }

    /**
     * prepareAsync prepare the MediaPlayer asynchronously
     *
     * @param id the key of MediaPlayer
     */
    public void prepareAsync(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.prepareAsync();
    }

    /**
     * play play the MediaPlayer
     *
     * @param id the key of MediaPlayer
     */
    public void play(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.start();
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_STARTED);
    }

    /**
     * pause pause the MediaPlayer
     *
     * @param id the key of MediaPlayer
     */
    public void pause(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.pause();
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_PAUSED);
    }

    /**
     * stop stop the MediaPlayer
     *
     * @param id the key of MediaPlayer
     */
    public void stop(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.stop();
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_STOPPED);
    }

    /**
     * reset reset the MediaPlayer
     *
     * @param id the id of MediaPlayer
     */
    public void reset(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.reset();
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_IDLE);
    }

    /**
     * release release the MediaPlayer
     *
     * @param id the id of MediaPlayer
     */
    public void release(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mediaPlayerMap.remove(id);
        mp.release();
        notifyInfo(id, PLAYER_INFO_TYPE_STATE_CHANGE, PLAYER_STATE_RELEASED);
    }

    /**
     * seekTo seek to the specified time position
     *
     * @param id the id of MediaPlayer
     * @param msec the time position to seek to
     * @param mode the mode of seek
     */
    public void seekTo(long id, int msec, int mode) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.seekTo(msec, mode);
    }

    /**
     * setVolume set the volume of the MediaPlayer
     *
     * @param id the id of MediaPlayer
     * @param leftVolume the left volume
     * @param rightVolume the right volume
     */
    public void setVolume(long id, float leftVolume, float rightVolume) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.setVolume(leftVolume, rightVolume);
        nativeOnVolumnChanged(id, leftVolume);
    }

    /**
     * get track index
     *
     * @param id    player id
     * @param type  track type
     * @return track index
     */
    public int getTrackIndex(long id, int type) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return -1;
        }
        return mp.getSelectedTrack(type);
    }

    private String getTrackMime(MediaPlayer.TrackInfo info) {
        String str = info.toString();
        if (str.indexOf("mime=") != -1) {
            String subStr = str.substring(str.indexOf("mime="));
            return subStr.substring(PLAYER_CODEC_MIME_LENGTH, subStr.indexOf(","));
        }
        return "unknow";
    }

    /**
     * get track info string
     *
     * @param id    player id
     * @param type  track type
     * @param key   track key
     * @return track info string
     */
    public String getTrackInfoString(long id, int type, String key) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return "";
        }
        for (MediaPlayer.TrackInfo info : mp.getTrackInfo()) {
            if (info.getTrackType() == type) {
                if (PLAYER_TRACK_CODEC_MIME.equals(key)) {
                    return getTrackMime(info);
                }
                if (info.getFormat() != null) {
                    return info.getFormat().getString(key);
                }
                return "";
            }
        }
        return "";
    }

    /**
     * get looping
     *
     * @param id player id
     * @return looping
     */
    public boolean isLooping(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return false;
        }
        return mp.isLooping();
    }

    /**
     * set looping
     *
     * @param id player id
     * @param flag looping flag
     */
    public void setLooping(long id, boolean flag) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.setLooping(flag);
    }

    /**
     * get current position
     *
     * @param id player id
     * @return current position
     */
    public int getCurrentPosition(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return -1;
        }
        return mp.getCurrentPosition();
    }

    /**
     * get duration
     *
     * @param id player id
     * @return duration
     */
    public int getDuration(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return -1;
        }
        return mp.getDuration();
    }

    /**
     * setSurface set the surface of the MediaPlayer
     *
     * @param id the id of MediaPlayer
     * @param instanceId the instance id of AceSurfaceHolder
     * @param surfaceID the surface id of AceSurfaceHolder
     */
    public void setSurface(long id, int instanceId, long surfaceID) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }

        Class<?>[] classes = {int.class, long.class};
        Object result = invoke("ohos.ace.adapter.AceSurfaceHolder", "getSurface", classes, instanceId, surfaceID);
        if (result instanceof Surface) {
            mp.setSurface((Surface) result);
        }
    }

    private static Object invoke(String className, String methodName, Class<?>[] parameterTypes,
                                 Object... args) {
        Object value = null;

        try {
            Class<?> clz = Class.forName(className);
            Method method = clz.getMethod(methodName, parameterTypes);
            value = method.invoke(null, args);
        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
                | SecurityException | ClassNotFoundException e) {
            ALog.e(LOG_TAG, "invoke " + e.getClass().getSimpleName());
        } catch (Exception e) {
            ALog.e(LOG_TAG, "unknown Exception in invoke");
            e.printStackTrace();
        }
        return value;
    }

    /**
     * get video width
     *
     * @param id player id
     * @return video width
     */
    public int getVideoWidth(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return 0;
        }
        return mp.getVideoWidth();
    }

    /**
     * get video height
     *
     * @param id player id
     * @return video height
     */
    public int getVideoHeight(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return 0;
        }
        return mp.getVideoHeight();
    }

    /**
     * Set the video scaling mode.
     *
     * @param id the player id
     * @param mode the video scaling mode
     */
    public void setVideoScalingMode(long id, int mode) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        mp.setVideoScalingMode(mode);
    }

    private int convertSpeedToMode(float speed) {
        if (Float.compare(speed, 1.0f) < 0) {
            return 0;
        } else if (Float.compare(speed, 1.0f) == 0) {
            return 1;
        } else if (Float.compare(speed, 1.25f) <= 0) {
            return 2;
        } else if (Float.compare(speed, 1.75f) <= 0) {
            return 3;
        } else {
            return 4;
        }
    }

    private float convertModeToSpeed(int mode) {
        float speed = 1.0f;
        switch (mode) {
            case 0:
                speed = 0.75f;
                break;
            case 1:
                speed = 1.0f;
                break;
            case 2:
                speed = 1.25f;
                break;
            case 3:
                speed = 1.75f;
                break;
            case 4:
                speed = 2.0f;
                break;
            default:
                break;
        }
        return speed;
    }

    /**
     * Set the playback speed.
     *
     * @param id the player id
     * @param mode the playback speed
     */
    public void setPlaybackParams(long id, int mode) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        boolean isPlaying = mp.isPlaying();
        PlaybackParams param = mp.getPlaybackParams();
        param = param.setSpeed(convertModeToSpeed(mode));
        mp.setPlaybackParams(param);
        if (!isPlaying) {
            mp.pause();
        }
        notifyInfo(id, PLAYER_INFO_TYPE_SPEEDDONE, convertSpeedToMode(mp.getPlaybackParams().getSpeed()));
    }

    /**
     * Get the playback speed.
     *
     * @param id the player id
     * @return the playback speed
     */
    public int getPlaybackParams(long id) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return 0;
        }
        return convertSpeedToMode(mp.getPlaybackParams().getSpeed());
    }

    /**
     * Select the bitrate.
     *
     * @param id the player id
     * @param bitrate the bitrate
     */
    public void selectBitrate(long id, int bitrate) {
        MediaPlayer mp = getMediaPlayerById(id);
        if (mp == null) {
            return;
        }
        for (MediaPlayer.TrackInfo info : mp.getTrackInfo()) {
            if (info.getTrackType() == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) {
                if (info.getFormat() != null) {
                    info.getFormat().setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
                    notifyInfo(id, PLAYER_INFO_TYPE_BITRATEDONE, bitrate);
                }
                return;
            }
        }
    }

    /**
     * Init PlayerJni jni.
     */
    protected native void nativeInit();

    /**
     * nativeOnInfo jni.
     *
     * @param key key
     * @param what what
     * @param extra extra
     */
    protected native void nativeOnInfo(long key, int what, int extra);

    /**
     * nativeOnError jni.
     *
     * @param key key
     * @param code code
     */
    protected native void nativeOnError(long key, int code);

    /**
     * nativeOnCompletion jni.
     *
     * @param key key
     * @param percent percent
     */
    protected native void nativeOnBufferingUpdate(long key, int percent);

    /**
     * nativeOnSeekComplete jni.
     *
     * @param key key
     * @param position position
     */
    protected native void nativeOnSeekComplete(long key, int position);

    /**
     * nativeOnVideoSizeChanged jni.
     *
     * @param key key
     * @param width width
     * @param height height
     */
    protected native void nativeOnVideoSizeChanged(long key, int width, int height);

    /**
     * nativeOnVolumnChanged jni.
     *
     * @param key key
     * @param vol vol
     */
    protected native void nativeOnVolumnChanged(long key, float vol);

    /**
     * nativeReadAt jni.
     *
     * @param key key
     * @param position position
     * @param buffer buffer
     * @param offset offset
     * @param size size
     * @return int
     */
    protected native int nativeReadAt(long key, long position, byte[] buffer, int offset, int size);
}