import URLToolkit from 'url-toolkit';
import Fragment from '../Loaders/Fragment';
import Level from './Level';
import LevelKey from './LevelKey';
import AttrList from '../Utils/attr-list';
import logger from '../Utils/Logger';
import { MediaPlaylist, AudioGroup, MediaPlaylistType } from '../Interfaces/media-playlist';
import { PlaylistLevelType, SingleLevels } from '../Interfaces/loader';
import { isCodecType, CodecType } from '../Utils/codecs';

/* eslint-disable */

/**
 * M3U8 parser
 * @module
 */

// https://regex101.com is your friend
const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g

const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
    [
        /#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
        /|(?!#)([\S+ ?]+)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
        /|#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
        /|#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
        /|#.*/.source // All other non-segment oriented tags will match with all groups empty
    ].join(''),
    'g'
)

const LEVEL_PLAYLIST_REGEX_SLOW = /(?:(?:#(EXTM3U))|(?:#EXT-X-(PLAYLIST-TYPE):(.+))|(?:#EXT-X-(MEDIA-SEQUENCE): *(\d+))|(?:#EXT-X-(TARGETDURATION): *(\d+))|(?:#EXT-X-(KEY):(.+))|(?:#EXT-X-(START):(.+))|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DISCONTINUITY-SEQ)UENCE:(\d+))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(VERSION):(\d+))|(?:#EXT-X-(MAP):(.+))|(?:(#)([^:]*):(.*))|(?:(#)(.*))(?:.*)\r?\n?/

const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i

export default class M3U8Parser {
    static Tag: string = 'M3U8Parser'

    static findGroup(groups: Array<AudioGroup>, mediaGroupId: string): AudioGroup | null {
        if (!groups) {
            return null
        }

        let matchingGroup = null

        for (let i = 0; i < groups.length; i++) {
            const group = groups[i]
            if (group.id === mediaGroupId) {
                matchingGroup = group
            }
        }

        return matchingGroup
    }

    static convertAVC1ToAVCOTI(codec: string) {
        let avcdata: Array<any> = codec.split('.')
        let result
        if (avcdata.length > 2) {
            result = avcdata.shift() + '.'
            result += parseInt(avcdata.shift()).toString(16)
            result += ('000' + parseInt(avcdata.shift()).toString(16)).substr(-4)
        } else {
            result = codec
        }
        return result
    }

    static resolve(url: string, baseUrl: string): string {
        return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true })
    }

    static parseMasterPlaylist(string: string, baseurl: string): SingleLevels[] {
        let levels: Array<any> = []
        MASTER_PLAYLIST_REGEX.lastIndex = 0

        function setCodecs(codecs: Array<string>, level: any) {
            ;['video', 'audio'].forEach(type => {
                const filtered = codecs.filter(codec => isCodecType(codec, type as CodecType))
                if (filtered.length) {
                    const preferred = filtered.filter(codec => {
                        return (
                            codec.lastIndexOf('avc1', 0) === 0 || codec.lastIndexOf('mp4a', 0) === 0
                        )
                    })
                    level[`${type}Codec`] = preferred.length > 0 ? preferred[0] : filtered[0]

                    // remove from list
                    codecs = codecs.filter(codec => filtered.indexOf(codec) === -1)
                }
            })

            level.unknownCodecs = codecs
        }

        let result: RegExpExecArray | null
        while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) {
            const level: any = {}

            const attrs = (level.attrs = new AttrList(result[1]))
            level.url = M3U8Parser.resolve(result[2], baseurl)

            const resolution = attrs.decimalResolution('RESOLUTION')
            if (resolution) {
                level.width = resolution.width
                level.height = resolution.height
            }
            level.bitrate =
                attrs.decimalInteger('AVERAGE-BANDWIDTH') || attrs.decimalInteger('BANDWIDTH')
            level.name = (<any>attrs).NAME

            setCodecs([].concat(((<any>attrs).CODECS || '').split(/[ ,]+/)), level)

            if (level.videoCodec && level.videoCodec.indexOf('avc1') !== -1) {
                level.videoCodec = M3U8Parser.convertAVC1ToAVCOTI(level.videoCodec)
            }

            levels.push(level)
        }
        return levels
    }

    static parseMasterPlaylistMedia(
        string: string,
        baseurl: string,
        type: MediaPlaylistType,
        audioGroups: Array<AudioGroup> = []
    ): Array<MediaPlaylist> {
        let result: RegExpExecArray | null
        let medias: Array<MediaPlaylist> = []
        let id = 0
        MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0
        while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
            const media = {}
            const attrs: any = new AttrList(result[1])
            if (attrs.TYPE === type) {
                const media: MediaPlaylist = {
                    id: id++,
                    groupId: attrs['GROUP-ID'],
                    name: attrs.NAME || attrs.LANGUAGE,
                    type,
                    default: attrs.DEFAULT === 'YES',
                    autoselect: attrs.AUTOSELECT === 'YES',
                    forced: attrs.FORCED === 'YES',
                    lang: attrs.LANGUAGE
                }

                if (attrs.URI) {
                    media.url = M3U8Parser.resolve(attrs.URI, baseurl)
                }

                if (audioGroups.length) {
                    const groupCodec = M3U8Parser.findGroup(audioGroups, <string>media.groupId)
                    media.audioCodec = groupCodec ? groupCodec.codec : audioGroups[0].codec
                }

                medias.push(media)
            }
        }
        return medias
    }

    static parseLevelPlaylist(
        string: string,
        baseurl: string,
        id: number,
        type: PlaylistLevelType,
        levelUrlId: number
    ): Level {
        let currentSN = 0
        let totalduration = 0
        let level = new Level(baseurl)

        let levelkey: LevelKey | undefined // todo 不用传输吗？？？
        let cc = 0
        let prevFrag: Fragment | null = null
        let frag: Fragment | null = new Fragment()
        let result: RegExpExecArray | RegExpMatchArray | null
        let i: number

        let firstPdtIndex = null

        LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0

        while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
            const duration = result[1]
            if (duration) {
                // 持续时间
                frag.duration = parseFloat(duration)
                // avoid sliced strings    https://github.com/video-dev/hls.js/issues/939
                const title = (' ' + result[2]).slice(1)
                frag.title = title || null
                frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration])
            } else if (result[3]) {
                // url
                if (Number.isFinite(frag.duration)) {
                    const sn = currentSN++
                    frag.type = type
                    frag.start = totalduration
                    frag.levelkey = levelkey
                    frag.sn = sn
                    frag.level = id
                    frag.cc = cc
                    frag.urlId = levelUrlId
                    frag.baseurl = baseurl
                    // avoid sliced strings    https://github.com/video-dev/hls.js/issues/939
                    frag.relurl = (' ' + result[3]).slice(1)
                    assignProgramDateTime(frag, prevFrag)

                    level.fragments.push(frag)
                    prevFrag = frag
                    totalduration += frag.duration

                    frag = new Fragment()
                }
            } else if (result[4]) {
                // X-BYTERANGE
                const data = (' ' + result[4]).slice(1)
                if (prevFrag) {
                    frag.setByteRange(data, prevFrag)
                } else {
                    frag.setByteRange(data)
                }
            } else if (result[5]) {
                // PROGRAM-DATE-TIME
                // avoid sliced strings    https://github.com/video-dev/hls.js/issues/939
                frag.rawProgramDateTime = (' ' + result[5]).slice(1)
                frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime])
                if (firstPdtIndex === null) {
                    firstPdtIndex = level.fragments.length
                }
            } else {
                result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW) as RegExpExecArray

                for (i = 1; i < result.length; i++) {
                    if (typeof result[i] !== 'undefined') {
                        break
                    }
                }

                // avoid sliced strings    https://github.com/video-dev/hls.js/issues/939
                const value1 = (' ' + result[i + 1]).slice(1)
                const value2 = (' ' + result[i + 2]).slice(1)
                switch (result[i]) {
                    case '#':
                        frag.tagList.push(value2 ? [value1, value2] : [value1])
                        break
                    case 'PLAYLIST-TYPE':
                        level.type = value1.toUpperCase()
                        break
                    case 'MEDIA-SEQUENCE':
                        currentSN = level.startSN = parseInt(value1)
                        break
                    case 'TARGETDURATION':
                        level.targetduration = parseFloat(value1)
                        break
                    case 'VERSION':
                        level.version = parseInt(value1)
                        break
                    case 'EXTM3U':
                        break
                    case 'ENDLIST':
                        level.live = false
                        break
                    case 'DIS':
                        cc++
                        frag.tagList.push(['DIS'])
                        break
                    case 'DISCONTINUITY-SEQ':
                        cc = parseInt(value1)
                        break
                    case 'KEY': {
                        // https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
                        const decryptparams = value1
                        const keyAttrs: any = new AttrList(decryptparams)
                        const decryptmethod = keyAttrs.enumeratedString('METHOD')
                        const decrypturi = keyAttrs.URI
                        const decryptiv = keyAttrs.hexadecimalInteger('IV')

                        if (decryptmethod) {
                            levelkey = new LevelKey(baseurl, decrypturi)
                            if (
                                decrypturi &&
                                ['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(
                                    decryptmethod
                                ) >= 0
                            ) {
                                levelkey.method = decryptmethod
                                // URI to get the key
                                levelkey.baseuri = baseurl
                                levelkey.reluri = decrypturi
                                levelkey.key = null
                                // Initialization Vector (IV)
                                levelkey.iv = decryptiv
                            }
                        }
                        break
                    }
                    case 'START': {
                        const startAttrs = new AttrList(value1)
                        const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET')
                        // TIME-OFFSET can be 0
                        if (Number.isFinite(startTimeOffset)) {
                            level.startTimeOffset = startTimeOffset
                        }
                        break
                    }
                    case 'MAP': {
                        const mapAttrs: any = new AttrList(value1)
                        frag.relurl = mapAttrs.URI
                        if (mapAttrs.BYTERANGE) {
                            frag.setByteRange(mapAttrs.BYTERANGE)
                        }
                        frag.baseurl = baseurl
                        frag.level = id
                        frag.type = type
                        frag.sn = 'initSegment'
                        level.initSegment = frag
                        frag = new Fragment()
                        frag.rawProgramDateTime = level.initSegment.rawProgramDateTime
                        break
                    }
                    default:
                        logger.warn(this.Tag, `line parsed but not handled: ${result}`)
                        break
                }
            }
        }
        frag = prevFrag
        // logger.log('found ' + level.fragments.length + ' fragments');
        if (frag && !frag.relurl) {
            level.fragments.pop()
            totalduration -= frag.duration
        }
        level.totalduration = totalduration // ts 总时长
        level.averagetargetduration = totalduration / level.fragments.length // 计算平均时长
        level.endSN = currentSN - 1 // 最后ts文件的序列号
        level.startCC = level.fragments[0] ? level.fragments[0].cc : 0
        level.endCC = cc

        if (!level.initSegment && level.fragments.length) {
            // this is a bit lurky but HLS really has no other way to tell us
            // if the fragments are TS or MP4, except if we download them :/
            // but this is to be able to handle SIDX.
            if (level.fragments.every(frag => MP4_REGEX_SUFFIX.test(frag.relurl))) {
                logger.warn(
                    this.Tag,
                    'MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX'
                )

                frag = new Fragment()
                frag.relurl = level.fragments[0].relurl
                frag.baseurl = baseurl
                frag.level = id
                frag.type = type
                frag.sn = 'initSegment'

                level.initSegment = frag
                level.needSidxRanges = true
            }
        }

        /**
         * Backfill any missing PDT values
         "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after
        one or more Media Segment URIs, the client SHOULD extrapolate
        backward from that tag (using EXTINF durations and/or media
        timestamps) to associate dates with those segments."
        * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs
        * computed.
        */
        if (firstPdtIndex) {
            backfillProgramDateTimes(level.fragments, firstPdtIndex)
        }

        return level
    }
}

function backfillProgramDateTimes(fragments: any, startIndex: number): void {
    let fragPrev = fragments[startIndex]
    for (let i = startIndex - 1; i >= 0; i--) {
        const frag = fragments[i]
        frag.programDateTime = fragPrev.programDateTime - frag.duration * 1000
        fragPrev = frag
    }
}

function assignProgramDateTime(frag: any, prevFrag: any): void {
    if (frag.rawProgramDateTime) {
        frag.programDateTime = Date.parse(frag.rawProgramDateTime)
    } else if (prevFrag && prevFrag.programDateTime) {
        frag.programDateTime = prevFrag.endProgramDateTime
    }

    if (!Number.isFinite(frag.programDateTime)) {
        frag.programDateTime = null
        frag.rawProgramDateTime = null
    }
}
/* eslint-enable */
