package org.schabi.newpipe.extractor.services.youtube;

import static org.schabi.newpipe.extractor.MediaFormat.M4A;
import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4;
import static org.schabi.newpipe.extractor.MediaFormat.WEBM;
import static org.schabi.newpipe.extractor.MediaFormat.WEBMA;
import static org.schabi.newpipe.extractor.MediaFormat.WEBMA_OPUS;
import static org.schabi.newpipe.extractor.MediaFormat.v3GPP;
import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.AUDIO;
import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO;
import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO_ONLY;

import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.exceptions.ParsingException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import java.io.Serializable;

public class ItagItem implements Serializable {

    /**
     * List can be found here:
     * https://github.com/ytdl-org/youtube-dl/blob/e988fa4/youtube_dl/extractor/youtube.py#L1195
     */
    private static final ItagItem[] ITAG_LIST = {
            /////////////////////////////////////////////////////
            // VIDEO     ID  Type   Format  Resolution  FPS  ////
            /////////////////////////////////////////////////////
            new ItagItem(17, VIDEO, v3GPP, "144p"),
            new ItagItem(36, VIDEO, v3GPP, "240p"),

            new ItagItem(18, VIDEO, MPEG_4, "360p"),
            new ItagItem(34, VIDEO, MPEG_4, "360p"),
            new ItagItem(35, VIDEO, MPEG_4, "480p"),
            new ItagItem(59, VIDEO, MPEG_4, "480p"),
            new ItagItem(78, VIDEO, MPEG_4, "480p"),
            new ItagItem(22, VIDEO, MPEG_4, "720p"),
            new ItagItem(37, VIDEO, MPEG_4, "1080p"),
            new ItagItem(38, VIDEO, MPEG_4, "1080p"),

            new ItagItem(43, VIDEO, WEBM, "360p"),
            new ItagItem(44, VIDEO, WEBM, "480p"),
            new ItagItem(45, VIDEO, WEBM, "720p"),
            new ItagItem(46, VIDEO, WEBM, "1080p"),

            //////////////////////////////////////////////////////////////////
            // AUDIO     ID      ItagType          Format        Bitrate    //
            //////////////////////////////////////////////////////////////////
            new ItagItem(171, AUDIO, WEBMA, 128),
            new ItagItem(172, AUDIO, WEBMA, 256),
            new ItagItem(139, AUDIO, M4A, 48),
            new ItagItem(140, AUDIO, M4A, 128),
            new ItagItem(141, AUDIO, M4A, 256),
            new ItagItem(249, AUDIO, WEBMA_OPUS, 50),
            new ItagItem(250, AUDIO, WEBMA_OPUS, 70),
            new ItagItem(251, AUDIO, WEBMA_OPUS, 160),

            /// VIDEO ONLY ////////////////////////////////////////////
            //           ID      Type     Format  Resolution  FPS  ////
            ///////////////////////////////////////////////////////////
            new ItagItem(160, VIDEO_ONLY, MPEG_4, "144p"),
            new ItagItem(133, VIDEO_ONLY, MPEG_4, "240p"),
            new ItagItem(134, VIDEO_ONLY, MPEG_4, "360p"),
            new ItagItem(135, VIDEO_ONLY, MPEG_4, "480p"),
            new ItagItem(212, VIDEO_ONLY, MPEG_4, "480p"),
            new ItagItem(136, VIDEO_ONLY, MPEG_4, "720p"),
            new ItagItem(298, VIDEO_ONLY, MPEG_4, "720p60", 60),
            new ItagItem(137, VIDEO_ONLY, MPEG_4, "1080p"),
            new ItagItem(299, VIDEO_ONLY, MPEG_4, "1080p60", 60),
            new ItagItem(266, VIDEO_ONLY, MPEG_4, "2160p"),

            new ItagItem(278, VIDEO_ONLY, WEBM, "144p"),
            new ItagItem(242, VIDEO_ONLY, WEBM, "240p"),
            new ItagItem(243, VIDEO_ONLY, WEBM, "360p"),
            new ItagItem(244, VIDEO_ONLY, WEBM, "480p"),
            new ItagItem(245, VIDEO_ONLY, WEBM, "480p"),
            new ItagItem(246, VIDEO_ONLY, WEBM, "480p"),
            new ItagItem(247, VIDEO_ONLY, WEBM, "720p"),
            new ItagItem(248, VIDEO_ONLY, WEBM, "1080p"),
            new ItagItem(271, VIDEO_ONLY, WEBM, "1440p"),
            // #272 is either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
            new ItagItem(272, VIDEO_ONLY, WEBM, "2160p"),
            new ItagItem(302, VIDEO_ONLY, WEBM, "720p60", 60),
            new ItagItem(303, VIDEO_ONLY, WEBM, "1080p60", 60),
            new ItagItem(308, VIDEO_ONLY, WEBM, "1440p60", 60),
            new ItagItem(313, VIDEO_ONLY, WEBM, "2160p"),
            new ItagItem(315, VIDEO_ONLY, WEBM, "2160p60", 60),

            // Add new AV1 format video-only streams
            new ItagItem(394, VIDEO_ONLY, MPEG_4, "144p"),
            new ItagItem(395, VIDEO_ONLY, MPEG_4, "240p"),
            new ItagItem(396, VIDEO_ONLY, MPEG_4, "360p"),
            new ItagItem(397, VIDEO_ONLY, MPEG_4, "480p"),
            new ItagItem(398, VIDEO_ONLY, MPEG_4, "720p HFR"),
            new ItagItem(399, VIDEO_ONLY, MPEG_4, "1080p HFR"),
            new ItagItem(400, VIDEO_ONLY, MPEG_4, "1440p HFR"),
            new ItagItem(401, VIDEO_ONLY, MPEG_4, "2160p HFR"),
            new ItagItem(402, VIDEO_ONLY, MPEG_4, "4320p HFR"),


            // Add new hbr AV1 streams
            new ItagItem(698, VIDEO_ONLY, MPEG_4, "720p HFR High"),
            new ItagItem(699, VIDEO_ONLY, MPEG_4, "1080p HFR High"),
            new ItagItem(700, VIDEO_ONLY, MPEG_4, "1440p HFR High"),
            new ItagItem(701, VIDEO_ONLY, MPEG_4, "2160p HFR High"),
            new ItagItem(571, VIDEO_ONLY, MPEG_4, "4320p HFR High"),
    };

    /*//////////////////////////////////////////////////////////////////////////
    // Utils
    //////////////////////////////////////////////////////////////////////////*/

    public static boolean isSupported(final int itag) {
        for (final ItagItem item : ITAG_LIST) {
            if (itag == item.id) {
                return true;
            }
        }
        return false;
    }

    @Nonnull
    public static ItagItem getItag(final int itagId) throws ParsingException {
        for (final ItagItem item : ITAG_LIST) {
            if (itagId == item.id) {
                return new ItagItem(item);
            }
        }
        throw new ParsingException("itag " + itagId + " is not supported");
    }

    /*//////////////////////////////////////////////////////////////////////////
    // Static constants
    //////////////////////////////////////////////////////////////////////////*/

    public static final int AVERAGE_BITRATE_UNKNOWN = -1;
    public static final int SAMPLE_RATE_UNKNOWN = -1;
    public static final int FPS_NOT_APPLICABLE_OR_UNKNOWN = -1;
    public static final int TARGET_DURATION_SEC_UNKNOWN = -1;
    public static final int AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN = -1;
    public static final long CONTENT_LENGTH_UNKNOWN = -1;
    public static final long APPROX_DURATION_MS_UNKNOWN = -1;

    /*//////////////////////////////////////////////////////////////////////////
    // Constructors and misc
    //////////////////////////////////////////////////////////////////////////*/

    public enum ItagType {
        AUDIO,
        VIDEO,
        VIDEO_ONLY
    }

    /**
     * Call {@link #ItagItem(int, ItagType, MediaFormat, String, int)} with the fps set to 30.
     */
    public ItagItem(final int id,
                    final ItagType type,
                    final MediaFormat format,
                    final String resolution) {
        this.id = id;
        this.itagType = type;
        this.mediaFormat = format;
        this.resolutionString = resolution;
        this.fps = 30;
    }

    /**
     * Constructor for videos.
     */
    public ItagItem(final int id,
                    final ItagType type,
                    final MediaFormat format,
                    final String resolution,
                    final int fps) {
        this.id = id;
        this.itagType = type;
        this.mediaFormat = format;
        this.resolutionString = resolution;
        this.fps = fps;
    }

    public ItagItem(final int id,
                    final ItagType type,
                    final MediaFormat format,
                    final int avgBitrate) {
        this.id = id;
        this.itagType = type;
        this.mediaFormat = format;
        this.avgBitrate = avgBitrate;
    }

    /**
     * Copy constructor of the {@link ItagItem} class.
     *
     * @param itagItem the {@link ItagItem} to copy its properties into a new {@link ItagItem}
     */
    public ItagItem(@Nonnull final ItagItem itagItem) {
        this.mediaFormat = itagItem.mediaFormat;
        this.id = itagItem.id;
        this.itagType = itagItem.itagType;
        this.avgBitrate = itagItem.avgBitrate;
        this.sampleRate = itagItem.sampleRate;
        this.audioChannels = itagItem.audioChannels;
        this.resolutionString = itagItem.resolutionString;
        this.fps = itagItem.fps;
        this.bitrate = itagItem.bitrate;
        this.width = itagItem.width;
        this.height = itagItem.height;
        this.initStart = itagItem.initStart;
        this.initEnd = itagItem.initEnd;
        this.indexStart = itagItem.indexStart;
        this.indexEnd = itagItem.indexEnd;
        this.quality = itagItem.quality;
        this.codec = itagItem.codec;
        this.targetDurationSec = itagItem.targetDurationSec;
        this.approxDurationMs = itagItem.approxDurationMs;
        this.contentLength = itagItem.contentLength;
    }

    public MediaFormat getMediaFormat() {
        return mediaFormat;
    }

    private final MediaFormat mediaFormat;

    public final int id;
    public final ItagType itagType;

    // Audio fields
    /** @deprecated Use {@link #getAverageBitrate()} instead. */
    @Deprecated
    public int avgBitrate = AVERAGE_BITRATE_UNKNOWN;
    private int sampleRate = SAMPLE_RATE_UNKNOWN;
    private int audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;

    // Video fields
    /** @deprecated Use {@link #getResolutionString()} instead. */
    @Deprecated
    public String resolutionString;

    /** @deprecated Use {@link #getFps()} and {@link #setFps(int)} instead. */
    @Deprecated
    public int fps = FPS_NOT_APPLICABLE_OR_UNKNOWN;

    // Fields for Dash
    private int bitrate;
    private int width;
    private int height;
    private int initStart;
    private int initEnd;
    private int indexStart;
    private int indexEnd;
    private String quality;
    private String codec;
    private int targetDurationSec = TARGET_DURATION_SEC_UNKNOWN;
    private long approxDurationMs = APPROX_DURATION_MS_UNKNOWN;
    private long contentLength = CONTENT_LENGTH_UNKNOWN;

    public int getBitrate() {
        return bitrate;
    }

    public void setBitrate(final int bitrate) {
        this.bitrate = bitrate;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(final int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(final int height) {
        this.height = height;
    }

    /**
     * Get the frame rate.
     *
     * <p>
     * It is set to the {@code fps} value returned in the corresponding itag in the YouTube player
     * response.
     * </p>
     *
     * <p>
     * It defaults to the standard value associated with this itag.
     * </p>
     *
     * <p>
     * Note that this value is only known for video itags, so {@link
     * #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags.
     * </p>
     *
     * @return the frame rate or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
     */
    public int getFps() {
        return fps;
    }

    /**
     * Set the frame rate.
     *
     * <p>
     * It is only known for video itags, so {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} is set/used for
     * non video itags or if the sample rate value is less than or equal to 0.
     * </p>
     *
     * @param fps the frame rate
     */
    public void setFps(final int fps) {
        this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN;
    }

    public int getInitStart() {
        return initStart;
    }

    public void setInitStart(final int initStart) {
        this.initStart = initStart;
    }

    public int getInitEnd() {
        return initEnd;
    }

    public void setInitEnd(final int initEnd) {
        this.initEnd = initEnd;
    }

    public int getIndexStart() {
        return indexStart;
    }

    public void setIndexStart(final int indexStart) {
        this.indexStart = indexStart;
    }

    public int getIndexEnd() {
        return indexEnd;
    }

    public void setIndexEnd(final int indexEnd) {
        this.indexEnd = indexEnd;
    }

    public String getQuality() {
        return quality;
    }

    public void setQuality(final String quality) {
        this.quality = quality;
    }

    /**
     * Get the resolution string associated with this {@code ItagItem}.
     *
     * <p>
     * It is only known for video itags.
     * </p>
     *
     * @return the resolution string associated with this {@code ItagItem} or
     * {@code null}.
     */
    @Nullable
    public String getResolutionString() {
        return resolutionString;
    }

    public String getCodec() {
        return codec;
    }

    public void setCodec(final String codec) {
        this.codec = codec;
    }

    /**
     * Get the average bitrate.
     *
     * <p>
     * It is only known for audio itags, so {@link #AVERAGE_BITRATE_UNKNOWN} is always returned for
     * other itag types.
     * </p>
     *
     * <p>
     * Bitrate of video itags and precise bitrate of audio itags can be known using
     * {@link #getBitrate()}.
     * </p>
     *
     * @return the average bitrate or {@link #AVERAGE_BITRATE_UNKNOWN}
     * @see #getBitrate()
     */
    public int getAverageBitrate() {
        return avgBitrate;
    }

    /**
     * Get the sample rate.
     *
     * <p>
     * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is returned for non audio
     * itags, or if the sample rate is unknown.
     * </p>
     *
     * @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN}
     */
    public int getSampleRate() {
        return sampleRate;
    }

    /**
     * Set the sample rate.
     *
     * <p>
     * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non audio
     * itags, or if the sample rate value is less than or equal to 0.
     * </p>
     *
     * @param sampleRate the sample rate of an audio itag
     */
    public void setSampleRate(final int sampleRate) {
        this.sampleRate = sampleRate > 0 ? sampleRate : SAMPLE_RATE_UNKNOWN;
    }

    /**
     * Get the number of audio channels.
     *
     * <p>
     * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
     * returned for non audio itags, or if it is unknown.
     * </p>
     *
     * @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN}
     */
    public int getAudioChannels() {
        return audioChannels;
    }

    /**
     * Set the number of audio channels.
     *
     * <p>
     * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
     * set/used for non audio itags, or if the {@code audioChannels} value is less than or equal to
     * 0.
     * </p>
     *
     * @param audioChannels the number of audio channels of an audio itag
     */
    public void setAudioChannels(final int audioChannels) {
        this.audioChannels = audioChannels > 0
                ? audioChannels
                : AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
    }

    /**
     * Get the {@code targetDurationSec} value.
     *
     * <p>
     * This value is the average time in seconds of the duration of sequences of livestreams and
     * ended livestreams. It is only returned by YouTube for these stream types, and makes no sense
     * for videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for those.
     * </p>
     *
     * @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN}
     */
    public int getTargetDurationSec() {
        return targetDurationSec;
    }

    /**
     * Set the {@code targetDurationSec} value.
     *
     * <p>
     * This value is the average time in seconds of the duration of sequences of livestreams and
     * ended livestreams.
     * </p>
     *
     * <p>
     * It is only returned for these stream types by YouTube and makes no sense for videos, so
     * {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if this value is
     * less than or equal to 0.
     * </p>
     *
     * @param targetDurationSec the target duration of a segment of streams which are using the
     *                          live delivery method type
     */
    public void setTargetDurationSec(final int targetDurationSec) {
        this.targetDurationSec = targetDurationSec > 0
                ? targetDurationSec
                : TARGET_DURATION_SEC_UNKNOWN;
    }

    /**
     * Get the {@code approxDurationMs} value.
     *
     * <p>
     * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is
     * returned for other stream types or if this value is less than or equal to 0.
     * </p>
     *
     * @return the {@code approxDurationMs} value or {@link #APPROX_DURATION_MS_UNKNOWN}
     */
    public long getApproxDurationMs() {
        return approxDurationMs;
    }

    /**
     * Set the {@code approxDurationMs} value.
     *
     * <p>
     * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is
     * set/used for other stream types or if this value is less than or equal to 0.
     * </p>
     *
     * @param approxDurationMs the approximate duration of a DASH progressive stream, in
     *                         milliseconds
     */
    public void setApproxDurationMs(final long approxDurationMs) {
        this.approxDurationMs = approxDurationMs > 0
                ? approxDurationMs
                : APPROX_DURATION_MS_UNKNOWN;
    }

    /**
     * Get the {@code contentLength} value.
     *
     * <p>
     * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is
     * returned for other stream types or if this value is less than or equal to 0.
     * </p>
     *
     * @return the {@code contentLength} value or {@link #CONTENT_LENGTH_UNKNOWN}
     */
    public long getContentLength() {
        return contentLength;
    }

    /**
     * Set the content length of stream.
     *
     * <p>
     * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is
     * set/used for other stream types or if this value is less than or equal to 0.
     * </p>
     *
     * @param contentLength the content length of a DASH progressive stream
     */
    public void setContentLength(final long contentLength) {
        this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_UNKNOWN;
    }
}
